diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php index a5abce1320b..7397fbf44f4 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php @@ -164,9 +164,9 @@ protected function requireRootNodeTypeToBeUnoccupied( protected function requireTetheredDescendantNodeTypesToExist(NodeType $nodeType): void { // this getter throws if any of the child nodeTypes doesnt exist! - $childNodeTypes = $nodeType->getAutoCreatedChildNodes(); - foreach ($childNodeTypes as $childNodeType) { - $this->requireTetheredDescendantNodeTypesToExist($childNodeType); + $tetheredNodeTypes = $this->getNodeTypeManager()->getTetheredNodesConfigurationForNodeType($nodeType); + foreach ($tetheredNodeTypes as $tetheredNodeType) { + $this->requireTetheredDescendantNodeTypesToExist($tetheredNodeType); } } @@ -176,7 +176,7 @@ protected function requireTetheredDescendantNodeTypesToExist(NodeType $nodeType) */ protected function requireTetheredDescendantNodeTypesToNotBeOfTypeRoot(NodeType $nodeType): void { - foreach ($nodeType->getAutoCreatedChildNodes() as $tetheredChildNodeType) { + foreach ($this->getNodeTypeManager()->getTetheredNodesConfigurationForNodeType($nodeType) as $tetheredChildNodeType) { if ($tetheredChildNodeType->isOfType(NodeTypeName::ROOT_NODE_TYPE_NAME)) { throw new NodeTypeIsOfTypeRoot( 'Node type "' . $nodeType->name->value . '" for tethered descendant is of type root.', @@ -301,12 +301,12 @@ protected function requireNodeTypeConstraintsImposedByParentToBeMet( } if ( $nodeName - && $parentsNodeType->hasAutoCreatedChildNode($nodeName) - && !$parentsNodeType->getTypeOfAutoCreatedChildNode($nodeName)?->name->equals($nodeType->name) + && $parentsNodeType->hasTetheredNode($nodeName) + && !$this->getNodeTypeManager()->getTypeOfTetheredNode($parentsNodeType, $nodeName)->name->equals($nodeType->name) ) { throw new NodeConstraintException( 'Node type "' . $nodeType->name->value . '" does not match configured "' - . $parentsNodeType->getTypeOfAutoCreatedChildNode($nodeName)?->name->value + . $this->getNodeTypeManager()->getTypeOfTetheredNode($parentsNodeType, $nodeName)->name->value . '" for auto created child nodes for parent type "' . $parentsNodeType->name->value . '" with name "' . $nodeName->value . '"' ); @@ -324,8 +324,8 @@ protected function areNodeTypeConstraintsImposedByParentValid( } if ( $nodeName - && $parentsNodeType->hasAutoCreatedChildNode($nodeName) - && !$parentsNodeType->getTypeOfAutoCreatedChildNode($nodeName)?->name->equals($nodeType->name) + && $parentsNodeType->hasTetheredNode($nodeName) + && !$this->getNodeTypeManager()->getTypeOfTetheredNode($parentsNodeType, $nodeName)->name->equals($nodeType->name) ) { return false; } @@ -362,8 +362,8 @@ protected function areNodeTypeConstraintsImposedByGrandparentValid( ): bool { if ( $parentNodeName - && $grandParentsNodeType->hasAutoCreatedChildNode($parentNodeName) - && !$grandParentsNodeType->allowsGrandchildNodeType($parentNodeName->value, $nodeType) + && $grandParentsNodeType->hasTetheredNode($parentNodeName) + && !$this->getNodeTypeManager()->isNodeTypeAllowedAsChildToTetheredNode($grandParentsNodeType, $parentNodeName, $nodeType) ) { return false; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php index 2896af0cd29..fb8e7de7aa0 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php @@ -211,8 +211,8 @@ protected function checkConstraintsImposedByAncestors( } if ( $nodeAggregate->nodeName - && $parentsNodeType->hasAutoCreatedChildNode($nodeAggregate->nodeName) - && $parentsNodeType->getTypeOfAutoCreatedChildNode($nodeAggregate->nodeName)?->name + && $parentsNodeType->hasTetheredNode($nodeAggregate->nodeName) + && $this->nodeTypeManager->getTypeOfTetheredNode($parentsNodeType, $nodeAggregate->nodeName)->name !== $command->newNodeTypeName->value ) { throw new NodeConstraintException( @@ -232,9 +232,10 @@ protected function checkConstraintsImposedByAncestors( ); if ( $parentAggregate->nodeName - && $grandParentsNodeType->hasAutoCreatedChildNode($parentAggregate->nodeName) - && !$grandParentsNodeType->allowsGrandchildNodeType( - $parentAggregate->nodeName->value, + && $grandParentsNodeType->hasTetheredNode($parentAggregate->nodeName) + && !$this->nodeTypeManager->isNodeTypeAllowedAsChildToTetheredNode( + $grandParentsNodeType, + $parentAggregate->nodeName, $newNodeType ) ) { diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php index 46ef4e6a46b..8a7b8d12554 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php @@ -31,6 +31,7 @@ use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyType; use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; @@ -61,6 +62,8 @@ abstract protected function requireNodeTypeToBeOfTypeRoot(NodeType $nodeType): v abstract protected function getPropertyConverter(): PropertyConverter; + abstract protected function getNodeTypeManager(): NodeTypeManager; + private function handleCreateNodeAggregateWithNode( CreateNodeAggregateWithNode $command, ContentRepository $contentRepository @@ -203,6 +206,7 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( } $descendantNodeAggregateIds = self::populateNodeAggregateIds( $nodeType, + $this->getNodeTypeManager(), $command->tetheredDescendantNodeAggregateIds ); // Write the auto-created descendant node aggregate ids back to the command; @@ -282,7 +286,7 @@ private function handleTetheredChildNodes( ContentRepository $contentRepository, ): Events { $events = []; - foreach ($nodeType->getAutoCreatedChildNodes() as $rawNodeName => $childNodeType) { + foreach ($this->getNodeTypeManager()->getTetheredNodesConfigurationForNodeType($nodeType) as $rawNodeName => $childNodeType) { assert($childNodeType instanceof NodeType); $nodeName = NodeName::fromString($rawNodeName); $childNodePath = $nodePath @@ -343,6 +347,7 @@ private function createTetheredWithNode( protected static function populateNodeAggregateIds( NodeType $nodeType, + NodeTypeManager $nodeTypeManager, ?NodeAggregateIdsByNodePaths $nodeAggregateIds, NodePath $childPath = null ): NodeAggregateIdsByNodePaths { @@ -350,7 +355,7 @@ protected static function populateNodeAggregateIds( $nodeAggregateIds = NodeAggregateIdsByNodePaths::createEmpty(); } // TODO: handle Multiple levels of autocreated child nodes - foreach ($nodeType->getAutoCreatedChildNodes() as $rawChildName => $childNodeType) { + foreach ($nodeTypeManager->getTetheredNodesConfigurationForNodeType($nodeType) as $rawChildName => $childNodeType) { $childName = NodeName::fromString($rawChildName); $childPath = $childPath ? $childPath->appendPathSegment($childName) diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php index f174092536a..478bab25b41 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php @@ -26,6 +26,7 @@ use Neos\ContentRepository\Core\Feature\NodeTypeChange\Command\ChangeNodeAggregateType; use Neos\ContentRepository\Core\Feature\NodeTypeChange\Event\NodeAggregateTypeWasChanged; use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; @@ -49,6 +50,8 @@ */ trait NodeTypeChange { + abstract protected function getNodeTypeManager(): NodeTypeManager; + abstract protected function requireProjectedNodeAggregate( ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, @@ -89,6 +92,7 @@ abstract protected function areNodeTypeConstraintsImposedByGrandparentValid( abstract protected static function populateNodeAggregateIds( NodeType $nodeType, + NodeTypeManager $nodeTypeManager, NodeAggregateIdsByNodePaths $nodeAggregateIds, NodePath $childPath = null ): NodeAggregateIdsByNodePaths; @@ -158,6 +162,7 @@ private function handleChangeNodeAggregateType( **************/ $descendantNodeAggregateIds = static::populateNodeAggregateIds( $newNodeType, + $this->getNodeTypeManager(), $command->tetheredDescendantNodeAggregateIds ); // Write the auto-created descendant node aggregate ids back to the command; @@ -190,7 +195,7 @@ private function handleChangeNodeAggregateType( } // new tethered child nodes - $expectedTetheredNodes = $newNodeType->getAutoCreatedChildNodes(); + $expectedTetheredNodes = $this->getNodeTypeManager()->getTetheredNodesConfigurationForNodeType($newNodeType); foreach ($nodeAggregate->getNodes() as $node) { assert($node instanceof Node); foreach ($expectedTetheredNodes as $serializedTetheredNodeName => $expectedTetheredNodeType) { @@ -371,7 +376,7 @@ private function deleteObsoleteTetheredNodesWhenChangingNodeType( NodeType $newNodeType, ContentRepository $contentRepository ): Events { - $expectedTetheredNodes = $newNodeType->getAutoCreatedChildNodes(); + $expectedTetheredNodes = $this->getNodeTypeManager()->getTetheredNodesConfigurationForNodeType($newNodeType); $events = []; // find disallowed tethered nodes diff --git a/Neos.ContentRepository.Core/Classes/NodeType/ConstraintCheck.php b/Neos.ContentRepository.Core/Classes/NodeType/ConstraintCheck.php new file mode 100644 index 00000000000..91058e9b8b8 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/NodeType/ConstraintCheck.php @@ -0,0 +1,137 @@ + $constraints + */ + public function __construct( + private readonly array $constraints + ) { + } + + public function isNodeTypeAllowed(NodeType $nodeType): bool + { + $directConstraintsResult = $this->isNodeTypeAllowedByDirectConstraints($nodeType); + if ($directConstraintsResult !== null) { + return $directConstraintsResult; + } + + $inheritanceConstraintsResult = $this->isNodeTypeAllowedByInheritanceConstraints($nodeType); + if ($inheritanceConstraintsResult !== null) { + return $inheritanceConstraintsResult; + } + + if (isset($this->constraints['*'])) { + return (bool)$this->constraints['*']; + } + + return false; + } + + /** + * @return boolean|null true if the passed $nodeType is allowed by the $constraints, null if couldn't be decided + */ + protected function isNodeTypeAllowedByDirectConstraints(NodeType $nodeType): ?bool + { + if ($this->constraints === []) { + return true; + } + + if ( + array_key_exists($nodeType->name->value, $this->constraints) + && $this->constraints[$nodeType->name->value] === true + ) { + return true; + } + + if ( + array_key_exists($nodeType->name->value, $this->constraints) + && $this->constraints[$nodeType->name->value] === false + ) { + return false; + } + + return null; + } + + /** + * This method loops over the constraints and finds node types that the given node type inherits from. For all + * matched super types, their super types are traversed to find the closest super node with a constraint which + * is used to evaluated if the node type is allowed. It finds the closest results for true and false, and uses + * the distance to choose which one wins (lowest). If no result is found the node type is allowed. + * + * @return ?boolean (null if no constraint matched) + */ + protected function isNodeTypeAllowedByInheritanceConstraints(NodeType $nodeType): ?bool + { + $constraintDistanceForTrue = null; + $constraintDistanceForFalse = null; + foreach ($this->constraints as $superType => $constraint) { + if ($nodeType->isOfType($superType)) { + $distance = $this->traverseSuperTypes($nodeType, $superType, 0); + + if ( + $constraint === true + && ($constraintDistanceForTrue === null || $constraintDistanceForTrue > $distance) + ) { + $constraintDistanceForTrue = $distance; + } + if ( + $constraint === false + && ($constraintDistanceForFalse === null || $constraintDistanceForFalse > $distance) + ) { + $constraintDistanceForFalse = $distance; + } + } + } + + if ($constraintDistanceForTrue !== null && $constraintDistanceForFalse !== null) { + return $constraintDistanceForTrue < $constraintDistanceForFalse; + } + + if ($constraintDistanceForFalse !== null) { + return false; + } + + if ($constraintDistanceForTrue !== null) { + return true; + } + + return null; + } + + /** + * This method traverses the given node type to find the first super type that matches the constraint node type. + * In case the hierarchy has more than one way of finding a path to the node type it's not taken into account, + * since the first matched is returned. This is accepted on purpose for performance reasons and due to the fact + * that such hierarchies should be avoided. + * + * Returns null if no NodeType matched + */ + protected function traverseSuperTypes( + NodeType $currentNodeType, + string $constraintNodeTypeName, + int $distance + ): ?int { + if ($currentNodeType->name->value === $constraintNodeTypeName) { + return $distance; + } + + $distance++; + foreach ($currentNodeType->getDeclaredSuperTypes() as $superType) { + $result = $this->traverseSuperTypes($superType, $constraintNodeTypeName, $distance); + if ($result !== null) { + return $result; + } + } + + return null; + } +} diff --git a/Neos.ContentRepository.Core/Classes/NodeType/Exception/TetheredNodeNotConfigured.php b/Neos.ContentRepository.Core/Classes/NodeType/Exception/TetheredNodeNotConfigured.php new file mode 100644 index 00000000000..00f6c4fd3e1 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/NodeType/Exception/TetheredNodeNotConfigured.php @@ -0,0 +1,10 @@ + $declaredSuperTypes Parent types of this node type * @param array $configuration the configuration for this node type which is defined in the schema * @throws \InvalidArgumentException + * + * @internal */ public function __construct( NodeTypeName $name, array $declaredSuperTypes, array $configuration, - private readonly NodeTypeManager $nodeTypeManager, private readonly NodeLabelGeneratorFactoryInterface $nodeLabelGeneratorFactory ) { $this->name = $name; @@ -297,6 +291,7 @@ public function isOfType(string|NodeTypeName $nodeTypeName): bool return true; } } + return false; } @@ -322,6 +317,7 @@ public function getLocalConfiguration(): array public function getFullConfiguration(): array { $this->initialize(); + return $this->fullConfiguration; } @@ -345,6 +341,7 @@ public function hasConfiguration(string $configurationPath): bool public function getConfiguration(string $configurationPath): mixed { $this->initialize(); + return ObjectAccess::getPropertyPath($this->fullConfiguration, $configurationPath); } @@ -439,234 +436,50 @@ public function getDefaultValuesForProperties(): array } /** - * Return an array with child nodes which should be automatically created - * - * @return array the key of this array is the name of the child, and the value its NodeType. - * @api + * @return bool true if $nodeName is an autocreated child node, false otherwise */ - public function getAutoCreatedChildNodes(): array + public function hasTetheredNode(NodeName $nodeName): bool { $this->initialize(); - if (!isset($this->fullConfiguration['childNodes'])) { - return []; - } - - $autoCreatedChildNodes = []; - foreach ($this->fullConfiguration['childNodes'] as $childNodeName => $childNodeConfiguration) { - if (isset($childNodeConfiguration['type'])) { - $autoCreatedChildNodes[NodeName::transliterateFromString($childNodeName)->value] - = $this->nodeTypeManager->getNodeType($childNodeConfiguration['type']); + foreach ($this->fullConfiguration['childNodes'] ?? [] as $rawChildNodeName => $configurationForChildNode) { + if (isset($configurationForChildNode['type'])) { + if (NodeName::transliterateFromString($rawChildNodeName)->equals($nodeName)) { + return true; + } } } - - return $autoCreatedChildNodes; + return false; } /** - * @return bool true if $nodeName is an autocreated child node, false otherwise + * @throws TetheredNodeNotConfigured if the requested tethred node is not configured. Check via {@see NodeType::hasTetheredNode()}. */ - public function hasAutoCreatedChildNode(NodeName $nodeName): bool + public function getNodeTypeNameOfTetheredNode(NodeName $nodeName): NodeTypeName { $this->initialize(); - return isset($this->fullConfiguration['childNodes'][$nodeName->value]); - } - - /** - * @throws NodeTypeNotFoundException - */ - public function getTypeOfAutoCreatedChildNode(NodeName $nodeName): ?NodeType - { - return isset($this->fullConfiguration['childNodes'][$nodeName->value]['type']) - ? $this->nodeTypeManager->getNodeType($this->fullConfiguration['childNodes'][$nodeName->value]['type']) - : null; + foreach ($this->fullConfiguration['childNodes'] ?? [] as $rawChildNodeName => $configurationForChildNode) { + if (isset($configurationForChildNode['type'])) { + if (NodeName::transliterateFromString($rawChildNodeName)->equals($nodeName)) { + return NodeTypeName::fromString($configurationForChildNode['type']); + } + } + } + throw new TetheredNodeNotConfigured(sprintf('The child node "%s" is not configured for node type "%s"', $nodeName->value, $this->name->value), 1694786811); } - /** * Checks if the given NodeType is acceptable as sub-node with the configured constraints, * not taking constraints of auto-created nodes into account. Thus, this method only returns * the correct result if called on NON-AUTO-CREATED nodes! * - * Otherwise, allowsGrandchildNodeType() needs to be called on the *parent node type*. + * Otherwise, isNodeTypeAllowedAsChildToTetheredNode() needs to be called on the *parent node type*. * * @return boolean true if the $nodeType is allowed as child node, false otherwise. */ public function allowsChildNodeType(NodeType $nodeType): bool { $constraints = $this->getConfiguration('constraints.nodeTypes') ?: []; - - return $this->isNodeTypeAllowedByConstraints($nodeType, $constraints); - } - - /** - * Checks if the given $nodeType is allowed as a childNode of the given $childNodeName - * (which must be auto-created in $this NodeType). - * - * Only allowed to be called if $childNodeName is auto-created. - * - * @param string $childNodeName The name of a configured childNode of this NodeType - * @param NodeType $nodeType The NodeType to check constraints for. - * @return bool true if the $nodeType is allowed as grandchild node, false otherwise. - * @throws \InvalidArgumentException If the given $childNodeName is not configured to be auto-created in $this. - */ - public function allowsGrandchildNodeType(string $childNodeName, NodeType $nodeType): bool - { - $autoCreatedChildNodes = $this->getAutoCreatedChildNodes(); - if (!isset($autoCreatedChildNodes[$childNodeName])) { - throw new \InvalidArgumentException( - 'The method "allowsGrandchildNodeType" can only be used on auto-created childNodes, ' - . 'given $childNodeName "' . $childNodeName . '" is not auto-created.', - 1403858395 - ); - } - $constraints = $autoCreatedChildNodes[$childNodeName]->getConfiguration('constraints.nodeTypes') ?: []; - - $childNodeConfiguration = []; - foreach ($this->getConfiguration('childNodes') as $name => $configuration) { - $childNodeConfiguration[NodeName::transliterateFromString($name)->value] = $configuration; - } - $childNodeConstraintConfiguration = ObjectAccess::getPropertyPath( - $childNodeConfiguration, - $childNodeName . '.constraints.nodeTypes' - ) ?: []; - - $constraints = Arrays::arrayMergeRecursiveOverrule($constraints, $childNodeConstraintConfiguration); - - return $this->isNodeTypeAllowedByConstraints($nodeType, $constraints); - } - - /** - * Internal method to check whether the passed-in $nodeType is allowed by the $constraints array. - * - * $constraints is an associative array where the key is the Node Type Name. If the value is "true", - * the node type is explicitly allowed. If the value is "false", the node type is explicitly denied. - * If nothing is specified, the fallback "*" is used. If that one is also not specified, we DENY by - * default. - * - * Super types of the given node types are also checked, so if a super type is constrained - * it will also take affect on the inherited node types. The closest constrained super type match is used. - * - * @param array $constraints - */ - protected function isNodeTypeAllowedByConstraints(NodeType $nodeType, array $constraints): bool - { - $directConstraintsResult = $this->isNodeTypeAllowedByDirectConstraints($nodeType, $constraints); - if ($directConstraintsResult !== null) { - return $directConstraintsResult; - } - - $inheritanceConstraintsResult = $this->isNodeTypeAllowedByInheritanceConstraints($nodeType, $constraints); - if ($inheritanceConstraintsResult !== null) { - return $inheritanceConstraintsResult; - } - - if (isset($constraints['*'])) { - return (bool)$constraints['*']; - } - - return false; - } - - /** - * @param array $constraints - * @return boolean true if the passed $nodeType is allowed by the $constraints - */ - protected function isNodeTypeAllowedByDirectConstraints(NodeType $nodeType, array $constraints): ?bool - { - if ($constraints === []) { - return true; - } - - if ( - array_key_exists($nodeType->name->value, $constraints) - && $constraints[$nodeType->name->value] === true - ) { - return true; - } - - if ( - array_key_exists($nodeType->name->value, $constraints) - && $constraints[$nodeType->name->value] === false - ) { - return false; - } - - return null; - } - - /** - * This method loops over the constraints and finds node types that the given node type inherits from. For all - * matched super types, their super types are traversed to find the closest super node with a constraint which - * is used to evaluated if the node type is allowed. It finds the closest results for true and false, and uses - * the distance to choose which one wins (lowest). If no result is found the node type is allowed. - * - * @param array $constraints - * @return ?boolean (null if no constraint matched) - */ - protected function isNodeTypeAllowedByInheritanceConstraints(NodeType $nodeType, array $constraints): ?bool - { - $constraintDistanceForTrue = null; - $constraintDistanceForFalse = null; - foreach ($constraints as $superType => $constraint) { - if ($nodeType->isOfType($superType)) { - $distance = $this->traverseSuperTypes($nodeType, $superType, 0); - - if ( - $constraint === true - && ($constraintDistanceForTrue === null || $constraintDistanceForTrue > $distance) - ) { - $constraintDistanceForTrue = $distance; - } - if ( - $constraint === false - && ($constraintDistanceForFalse === null || $constraintDistanceForFalse > $distance) - ) { - $constraintDistanceForFalse = $distance; - } - } - } - - if ($constraintDistanceForTrue !== null && $constraintDistanceForFalse !== null) { - return $constraintDistanceForTrue < $constraintDistanceForFalse; - } - - if ($constraintDistanceForFalse !== null) { - return false; - } - - if ($constraintDistanceForTrue !== null) { - return true; - } - - return null; - } - - /** - * This method traverses the given node type to find the first super type that matches the constraint node type. - * In case the hierarchy has more than one way of finding a path to the node type it's not taken into account, - * since the first matched is returned. This is accepted on purpose for performance reasons and due to the fact - * that such hierarchies should be avoided. - * - * Returns null if no NodeType matched - */ - protected function traverseSuperTypes( - NodeType $currentNodeType, - string $constraintNodeTypeName, - int $distance - ): ?int { - if ($currentNodeType->name->value === $constraintNodeTypeName) { - return $distance; - } - - $distance++; - foreach ($currentNodeType->getDeclaredSuperTypes() as $superType) { - $result = $this->traverseSuperTypes($superType, $constraintNodeTypeName, $distance); - if ($result !== null) { - return $result; - } - } - - return null; + return (new ConstraintCheck($constraints))->isNodeTypeAllowed($nodeType); } /** diff --git a/Neos.ContentRepository.Core/Classes/NodeType/NodeTypeManager.php b/Neos.ContentRepository.Core/Classes/NodeType/NodeTypeManager.php index 9fa0f00408b..d01f0158edc 100644 --- a/Neos.ContentRepository.Core/Classes/NodeType/NodeTypeManager.php +++ b/Neos.ContentRepository.Core/Classes/NodeType/NodeTypeManager.php @@ -14,9 +14,13 @@ namespace Neos\ContentRepository\Core\NodeType; +use Neos\ContentRepository\Core\NodeType\Exception\TetheredNodeNotConfigured; use Neos\ContentRepository\Core\SharedModel\Exception\NodeConfigurationException; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeIsFinalException; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; +use Neos\ContentRepository\Core\SharedModel\Node\NodeName; +use Neos\Utility\Arrays; +use Neos\Utility\Exception\PropertyNotAccessibleException; /** * Manager for node types @@ -182,6 +186,80 @@ public function overrideNodeTypes(array $completeNodeTypeConfiguration): void } } + /** + * @param NodeType $nodeType + * @param NodeName $tetheredNodeName + * @return NodeType + *@throws TetheredNodeNotConfigured if the requested tethered node is not configured. Check via {@see NodeType::hasTetheredNode()}. + */ + public function getTypeOfTetheredNode(NodeType $nodeType, NodeName $tetheredNodeName): NodeType + { + $nameOfTetheredNode = $nodeType->getNodeTypeNameOfTetheredNode($tetheredNodeName); + return $this->getNodeType($nameOfTetheredNode); + } + + /** + * Return an array with child nodes which should be automatically created + * + * @return array the key of this array is the name of the child, and the value its NodeType. + * @api + */ + public function getTetheredNodesConfigurationForNodeType(NodeType $nodeType): array + { + $childNodeConfiguration = $nodeType->getConfiguration('childNodes'); + $autoCreatedChildNodes = []; + foreach ($childNodeConfiguration ?? [] as $childNodeName => $configurationForChildNode) { + if (isset($configurationForChildNode['type'])) { + $autoCreatedChildNodes[NodeName::transliterateFromString($childNodeName)->value] = $this->getNodeType($configurationForChildNode['type']); + } + } + return $autoCreatedChildNodes; + } + + /** + * Checks if the given $nodeType is allowed as a childNode of the given $tetheredNodeName + * (which must be tethered in $parentNodeType). + * + * Only allowed to be called if $tetheredNodeName is actually tethered. + * + * @param NodeType $parentNodeType The NodeType to check constraints based on. + * @param NodeName $tetheredNodeName The name of a configured tethered node of this NodeType + * @param NodeType $nodeType The NodeType to check constraints for. + * @return bool true if the $nodeType is allowed as grandchild node, false otherwise. + * @throws \InvalidArgumentException if the requested tethered node is not configured in the parent NodeType. Check via {@see NodeType::hasTetheredNode()}. + */ + public function isNodeTypeAllowedAsChildToTetheredNode(NodeType $parentNodeType, NodeName $tetheredNodeName, NodeType $nodeType): bool + { + try { + $typeOfTetheredNode = $this->getTypeOfTetheredNode($parentNodeType, $tetheredNodeName); + } catch (TetheredNodeNotConfigured $exception) { + throw new \InvalidArgumentException( + sprintf( + 'Cannot determine if grandchild is allowed in %s. Because the given child node name "%s" is not auto-created.', + $parentNodeType->name->value, + $tetheredNodeName->value + ), + 1403858395, + $exception + ); + } + + // Constraints configured on the NodeType for the child node + $constraints = $typeOfTetheredNode->getConfiguration('constraints.nodeTypes') ?: []; + + // Constraints configured at the childNode configuration of the parent. + try { + $childNodeConstraintConfiguration = $parentNodeType->getConfiguration('childNodes.' . $tetheredNodeName->value . '.constraints.nodeTypes') ?? []; + } catch (PropertyNotAccessibleException $exception) { + // We ignore this because the configuration might just not have any constraints, if the childNode was not configured the exception above would have been thrown. + $childNodeConstraintConfiguration = []; + } + + $constraints = Arrays::arrayMergeRecursiveOverrule($constraints, $childNodeConstraintConfiguration); + + return (new ConstraintCheck($constraints))->isNodeTypeAllowed($nodeType); + } + /** * Load one node type, if it is not loaded yet. * @@ -238,7 +316,6 @@ protected function loadNodeType(string $nodeTypeName, array &$completeNodeTypeCo NodeTypeName::fromString($nodeTypeName), $superTypes, $nodeTypeConfiguration, - $this, $this->nodeLabelGeneratorFactory ); diff --git a/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeManagerTest.php b/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeManagerTest.php index 60dfb8741bd..0592c8e3d44 100644 --- a/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeManagerTest.php +++ b/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeManagerTest.php @@ -12,6 +12,8 @@ */ use Neos\ContentRepository\Core\NodeType\DefaultNodeLabelGeneratorFactory; +use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\SharedModel\Exception\NodeConfigurationException; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeIsFinalException; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; @@ -121,6 +123,11 @@ protected function prepareNodeTypeManager(array $nodeTypesFixtureData) ], 'Neos.ContentRepository.Testing:Page2' => [ 'superTypes' => ['Neos.ContentRepository.Testing:Document' => true], + 'childNodes' => [ + 'nodeName' => [ + 'type' => 'Neos.ContentRepository.Testing:Document' + ] + ] ], 'Neos.ContentRepository.Testing:Page3' => [ 'superTypes' => ['Neos.ContentRepository.Testing:Document' => true], @@ -335,4 +342,15 @@ public function getSubNodeTypesWithDifferentIncludeFlagValuesReturnsCorrectValue $subNodeTypes = $this->nodeTypeManager->getSubNodeTypes('Neos.ContentRepository.Testing:ContentObject', false); self::assertArrayNotHasKey('Neos.ContentRepository.Testing:AbstractType', $subNodeTypes); } + + /** + * @test + */ + public function getAutoCreatedChildNodesReturnsLowercaseNames() + { + $parentNodeType = $this->nodeTypeManager->getNodeType(NodeTypeName::fromString('Neos.ContentRepository.Testing:Page2')); + $autoCreatedChildNodes = $this->nodeTypeManager->getTetheredNodesConfigurationForNodeType($parentNodeType); + // This is configured as "nodeName" above, but should be normalized to "nodename" + self::assertArrayHasKey('nodename', $autoCreatedChildNodes); + } } diff --git a/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeTest.php b/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeTest.php index bd38fc10066..3777e0c725c 100644 --- a/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeTest.php +++ b/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeTest.php @@ -22,7 +22,6 @@ * Testcase for the "NodeType" domain model */ class NodeTypeTest extends TestCase - { /** * example node types @@ -130,9 +129,7 @@ class NodeTypeTest extends TestCase */ public function aNodeTypeHasAName() { - $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository.Testing:Text'), [], [], - $this->getMockBuilder(NodeTypeManager::class) - ->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository.Testing:Text'), [], [], new DefaultNodeLabelGeneratorFactory()); self::assertSame('Neos.ContentRepository.Testing:Text', $nodeType->name->value); } @@ -142,9 +139,7 @@ public function aNodeTypeHasAName() public function setDeclaredSuperTypesExpectsAnArrayOfNodeTypesAsKeys() { $this->expectException(\InvalidArgumentException::class); - new NodeType(NodeTypeName::fromString('ContentRepository:Folder'), ['foo' => true], [], - $this->getMockBuilder(NodeTypeManager::class) - ->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory() + new NodeType(NodeTypeName::fromString('ContentRepository:Folder'), ['foo' => true], [], new DefaultNodeLabelGeneratorFactory() ); } @@ -154,8 +149,7 @@ public function setDeclaredSuperTypesExpectsAnArrayOfNodeTypesAsKeys() public function setDeclaredSuperTypesAcceptsAnArrayOfNodeTypes() { $this->expectException(\InvalidArgumentException::class); - new NodeType(NodeTypeName::fromString('ContentRepository:Folder'), ['foo'], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + new NodeType(NodeTypeName::fromString('ContentRepository:Folder'), ['foo'], [], new DefaultNodeLabelGeneratorFactory()); } /** @@ -163,13 +157,11 @@ public function setDeclaredSuperTypesAcceptsAnArrayOfNodeTypes() */ public function nodeTypesCanHaveAnyNumberOfSuperTypes() { - $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], new DefaultNodeLabelGeneratorFactory()); $timeableNodeType = new NodeType( NodeTypeName::fromString('Neos.ContentRepository.Testing:TimeableContent'), - [], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory() + [], [], new DefaultNodeLabelGeneratorFactory() ); $documentType = new NodeType( NodeTypeName::fromString('Neos.ContentRepository.Testing:Document'), @@ -177,13 +169,12 @@ public function nodeTypesCanHaveAnyNumberOfSuperTypes() 'Neos.ContentRepository:Base' => $baseType, 'Neos.ContentRepository.Testing:TimeableContent' => $timeableNodeType, ], - [], $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory() + [], new DefaultNodeLabelGeneratorFactory() ); $hideableNodeType = new NodeType( NodeTypeName::fromString('Neos.ContentRepository.Testing:HideableContent'), - [], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory() + [], [], new DefaultNodeLabelGeneratorFactory() ); $pageType = new NodeType( NodeTypeName::fromString('Neos.ContentRepository.Testing:Page'), @@ -193,7 +184,6 @@ public function nodeTypesCanHaveAnyNumberOfSuperTypes() 'Neos.ContentRepository.Testing:TimeableContent' => null, ], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory() ); @@ -219,8 +209,7 @@ public function nodeTypesCanHaveAnyNumberOfSuperTypes() */ public function labelIsEmptyStringByDefault() { - $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], new DefaultNodeLabelGeneratorFactory()); self::assertSame('', $baseType->getLabel()); } @@ -229,8 +218,7 @@ public function labelIsEmptyStringByDefault() */ public function propertiesAreEmptyArrayByDefault() { - $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], new DefaultNodeLabelGeneratorFactory()); self::assertSame([], $baseType->getProperties()); } @@ -243,7 +231,7 @@ public function hasConfigurationReturnsTrueIfSpecifiedConfigurationPathExists() 'someKey' => [ 'someSubKey' => 'someValue' ] - ], $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + ], new DefaultNodeLabelGeneratorFactory()); self::assertTrue($nodeType->hasConfiguration('someKey.someSubKey')); } @@ -252,8 +240,7 @@ public function hasConfigurationReturnsTrueIfSpecifiedConfigurationPathExists() */ public function hasConfigurationReturnsFalseIfSpecifiedConfigurationPathDoesNotExist() { - $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], new DefaultNodeLabelGeneratorFactory()); self::assertFalse($nodeType->hasConfiguration('some.nonExisting.path')); } @@ -266,7 +253,7 @@ public function getConfigurationReturnsTheConfigurationWithTheSpecifiedPath() 'someKey' => [ 'someSubKey' => 'someValue' ] - ], $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + ], new DefaultNodeLabelGeneratorFactory()); self::assertSame('someValue', $nodeType->getConfiguration('someKey.someSubKey')); } @@ -275,8 +262,7 @@ public function getConfigurationReturnsTheConfigurationWithTheSpecifiedPath() */ public function getConfigurationReturnsNullIfTheSpecifiedPathDoesNotExist() { - $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], new DefaultNodeLabelGeneratorFactory()); self::assertNull($nodeType->getConfiguration('some.nonExisting.path')); } @@ -401,25 +387,7 @@ protected function getNodeType(string $nodeTypeName): ?NodeType NodeTypeName::fromString($nodeTypeName), $declaredSuperTypes, $configuration, - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory() ); } - - /** - * @test - */ - public function getAutoCreatedChildNodesReturnsLowercasePaths() - { - $childNodeConfiguration = ['type' => 'Neos.ContentRepository:Base']; - $mockNodeTypeManager = $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(); - $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [ - 'childNodes' => ['nodeName' => $childNodeConfiguration] - ], $mockNodeTypeManager, new DefaultNodeLabelGeneratorFactory()); - $mockNodeTypeManager->expects(self::any())->method('getNodeType')->will(self::returnValue($baseType)); - - $autoCreatedChildNodes = $mockNodeTypeManager->getNodeType('Neos.ContentRepository:Base')->getAutoCreatedChildNodes(); - - self::assertArrayHasKey('nodename', $autoCreatedChildNodes); - } } diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php index ba5440ffc16..ea9ad36e5b5 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php @@ -393,7 +393,7 @@ private function isAutoCreatedChildNode(NodeTypeName $parentNodeTypeName, NodeNa return false; } $nodeTypeOfParent = $this->nodeTypeManager->getNodeType($parentNodeTypeName); - return $nodeTypeOfParent->hasAutoCreatedChildNode($nodeName); + return $nodeTypeOfParent->hasTetheredNode($nodeName); } private function dispatch(Severity $severity, string $message, mixed ...$args): void diff --git a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/DisallowedChildNodeAdjustment.php b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/DisallowedChildNodeAdjustment.php index a8b1c362855..d19b762ace8 100644 --- a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/DisallowedChildNodeAdjustment.php +++ b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/DisallowedChildNodeAdjustment.php @@ -62,7 +62,6 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): \Generat ? $subgraph->findParentNode($parentNode->nodeAggregateId) : null; - $allowedByParent = true; $parentNodeType = null; if ($parentNode !== null) { @@ -76,14 +75,15 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): \Generat $grandparentNodeType = null; if ( $parentNode !== null - && $grandparentNode != null + && $grandparentNode !== null && $parentNode->classification->isTethered() && !is_null($parentNode->nodeName) ) { - if ($this->nodeTypeManager->hasNodeType($grandparentNode->nodeTypeName)) { - $grandparentNodeType = $this->nodeTypeManager->getNodeType($grandparentNode->nodeTypeName); - $allowedByGrandparent = $grandparentNodeType->allowsGrandchildNodeType( - $parentNode->nodeName->value, + $grandparentNodeType = $this->nodeTypeManager->hasNodeType($grandparentNode->nodeTypeName) ? $this->nodeTypeManager->getNodeType($grandparentNode->nodeTypeName) : null; + if ($grandparentNodeType !== null) { + $allowedByGrandparent = $this->nodeTypeManager->isNodeTypeAllowedAsChildToTetheredNode( + $grandparentNodeType, + $parentNode->nodeName, $nodeType ); } @@ -97,10 +97,10 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): \Generat $message = sprintf( ' - The parent node type "%s" is not allowing children of type "%s", - and the grandparent node type "%s" is not allowing grandchildren of type "%s". - Thus, the node is invalid at this location and should be removed. - ', + The parent node type "%s" is not allowing children of type "%s", + and the grandparent node type "%s" is not allowing grandchildren of type "%s". + Thus, the node is invalid at this location and should be removed. + ', $parentNodeType !== null ? $parentNodeType->name->value : '', $node->nodeTypeName->value, $grandparentNodeType !== null ? $grandparentNodeType->name->value : '', @@ -123,6 +123,7 @@ function () use ($nodeAggregate, $coveredDimensionSpacePoint) { } } + private function removeNodeInSingleDimensionSpacePoint( NodeAggregate $nodeAggregate, DimensionSpacePoint $dimensionSpacePoint diff --git a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php index aa50bb0324b..994672b390e 100644 --- a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php +++ b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php @@ -51,9 +51,7 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): \Generat // In case we cannot find the expected tethered nodes, this fix cannot do anything. return; } - $nodeType = $this->nodeTypeManager->getNodeType($nodeTypeName); - - $expectedTetheredNodes = $nodeType->getAutoCreatedChildNodes(); + $expectedTetheredNodes = $this->nodeTypeManager->getTetheredNodesConfigurationForNodeType($this->nodeTypeManager->getNodeType($nodeTypeName)); foreach ($this->projectedNodeIterator->nodeAggregatesOfType($nodeTypeName) as $nodeAggregate) { // find missing tethered nodes diff --git a/Neos.Neos/Classes/Service/NodeTypeSchemaBuilder.php b/Neos.Neos/Classes/Service/NodeTypeSchemaBuilder.php index e626cfd37a8..b48f920c261 100644 --- a/Neos.Neos/Classes/Service/NodeTypeSchemaBuilder.php +++ b/Neos.Neos/Classes/Service/NodeTypeSchemaBuilder.php @@ -17,6 +17,7 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\SharedModel\Node\NodeName; /** * Renders the Node Type Schema in a format the User Interface understands; @@ -104,9 +105,9 @@ protected function generateConstraints() } } - foreach ($nodeType->getAutoCreatedChildNodes() as $key => $_x) { + foreach ($this->nodeTypeManager->getTetheredNodesConfigurationForNodeType($nodeType) as $key => $_x) { foreach ($nodeTypes as $innerNodeTypeName => $innerNodeType) { - if ($nodeType->allowsGrandchildNodeType($key, $innerNodeType)) { + if ($this->nodeTypeManager->isNodeTypeAllowedAsChildToTetheredNode($nodeType, NodeName::fromString($key), $innerNodeType)) { $constraints[$nodeTypeName]['childNodes'][$key]['nodeTypes'][$innerNodeTypeName] = true; } } diff --git a/Neos.Neos/Tests/Unit/NodeTypePostprocessor/DefaultPropertyEditorPostprocessorTest.php b/Neos.Neos/Tests/Unit/NodeTypePostprocessor/DefaultPropertyEditorPostprocessorTest.php index b0c1d52fafa..5d5723b2bec 100644 --- a/Neos.Neos/Tests/Unit/NodeTypePostprocessor/DefaultPropertyEditorPostprocessorTest.php +++ b/Neos.Neos/Tests/Unit/NodeTypePostprocessor/DefaultPropertyEditorPostprocessorTest.php @@ -14,7 +14,6 @@ use Neos\ContentRepository\Core\NodeType\DefaultNodeLabelGeneratorFactory; use Neos\ContentRepository\Core\NodeType\NodeType; -use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\Flow\Tests\UnitTestCase; use Neos\Neos\NodeTypePostprocessor\DefaultPropertyEditorPostprocessor; @@ -33,10 +32,6 @@ private function processConfiguration(array $configuration, array $dataTypesDefa NodeTypeName::fromString('Some.NodeType:Name'), [], [], - new NodeTypeManager( - fn () => [], - new DefaultNodeLabelGeneratorFactory() - ), new DefaultNodeLabelGeneratorFactory() ); $postprocessor->process($mockNodeType, $configuration, []);