diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/01-SetNodeReferences_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/01-SetNodeReferences_ConstraintChecks.feature index 58b46366b4c..c374e45a337 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/01-SetNodeReferences_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/01-SetNodeReferences_ConstraintChecks.feature @@ -12,6 +12,7 @@ Feature: Constraint checks on SetNodeReferences 'Neos.ContentRepository.Testing:ReferencedNode': [] 'Neos.ContentRepository.Testing:NodeWithReferences': + # legacy notation properties: referenceProperty: type: reference @@ -19,13 +20,16 @@ Feature: Constraint checks on SetNodeReferences type: references nonReferenceProperty: type: string + references: + constrainedReferenceCount: + constraints: + maxItems: 1 constrainedReferenceProperty: - type: reference constraints: + maxItems: 1 nodeTypes: 'Neos.ContentRepository.Testing:ReferencedNode': false referencePropertyWithProperties: - type: reference properties: text: type: string @@ -128,6 +132,22 @@ Feature: Constraint checks on SetNodeReferences | references | [{"target":"lady-eleonode-rootford"}] | Then the last command should have thrown an exception of type "NodeAggregateIsRoot" + Scenario: Try to set references exceeding the maxItems count + When the command SetNodeReferences is executed with payload and exceptions are caught: + | Key | Value | + | sourceNodeAggregateId | "source-nodandaise" | + | referenceName | "constrainedReferenceCount" | + | references | [{"target":"anthony-destinode"}, {"target":"berta-destinode"}] | + Then the last command should have thrown an exception of type "ReferenceCannotBeSet" with code 1700150156 + + Scenario: Try to set references exceeding the maxItems count for legacy property reference declaration + When the command SetNodeReferences is executed with payload and exceptions are caught: + | Key | Value | + | sourceNodeAggregateId | "source-nodandaise" | + | referenceName | "referenceProperty" | + | references | [{"target":"anthony-destinode"}, {"target":"berta-destinode"}] | + Then the last command should have thrown an exception of type "ReferenceCannotBeSet" with code 1700150156 + Scenario: Try to reference a node aggregate of a type not matching the constraints When the command SetNodeReferences is executed with payload and exceptions are caught: | Key | Value | @@ -187,3 +207,11 @@ Feature: Constraint checks on SetNodeReferences | referenceName | "referencePropertyWithProperties" | | references | [{"target":"anthony-destinode", "properties":{"postalAddress": "28 31st of February Street"}}] | Then the last command should have thrown an exception of type "ReferenceCannotBeSet" with code 1658406762 + + Scenario: Node reference cannot hold multiple targets to the same node + When the command SetNodeReferences is executed with payload and exceptions are caught: + | Key | Value | + | sourceNodeAggregateId | "source-nodandaise" | + | referenceName | "referencesProperty" | + | references | [{"target":"anthony-destinode"}, {"target":"anthony-destinode"}] | + Then the last command should have thrown an exception of type "InvalidArgumentException" with code 1700150910 diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/02-SetNodeReferences_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/02-SetNodeReferences_WithoutDimensions.feature index af2ac4fbb23..287ab61f562 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/02-SetNodeReferences_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/02-SetNodeReferences_WithoutDimensions.feature @@ -10,19 +10,21 @@ Feature: Node References without Dimensions 'Neos.ContentRepository.Testing:ReferencedNode': [] 'Neos.ContentRepository.Testing:NodeWithReferences': + # legacy notation properties: referenceProperty: type: reference referencesProperty: type: references + references: restrictedReferenceProperty: - type: reference constraints: nodeTypes: '*': false 'Neos.ContentRepository.Testing:ReferencedNode': true referencePropertyWithProperty: - type: reference + constraints: + maxItems: 1 properties: text: type: string @@ -31,7 +33,6 @@ Feature: Node References without Dimensions postalAddress: type: 'Neos\ContentRepository\Core\Tests\Behavior\Fixtures\PostalAddress' referencesPropertyWithProperty: - type: references properties: text: type: string diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeById.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeById.feature index 784145ebdd4..b61dffbc52f 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeById.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeById.feature @@ -12,13 +12,14 @@ Feature: Find nodes using the findNodeById query properties: text: type: string + references: refs: - type: references properties: foo: type: string ref: - type: reference + constraints: + maxItems: 1 properties: foo: type: string diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPath.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPath.feature index e729b32d65e..c895ac33025 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPath.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPath.feature @@ -15,13 +15,14 @@ Feature: Find nodes using the findNodeByPath query properties: text: type: string + references: refs: - type: references properties: foo: type: string ref: - type: reference + constraints: + maxItems: 1 properties: foo: type: string diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPathAsNodeName.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPathAsNodeName.feature index dd4a723f80d..bb98926abc2 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPathAsNodeName.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPathAsNodeName.feature @@ -12,13 +12,14 @@ Feature: Find nodes using the findNodeByPath query with node name as path argume properties: text: type: string + references: refs: - type: references properties: foo: type: string ref: - type: reference + constraints: + maxItems: 1 properties: foo: type: string diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindParentNode.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindParentNode.feature index e4e40665c0c..68bc44ef005 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindParentNode.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindParentNode.feature @@ -12,13 +12,14 @@ Feature: Find nodes using the findParentNodes query properties: text: type: string + references: refs: - type: references properties: foo: type: string ref: - type: reference + constraints: + maxItems: 1 properties: foo: type: string diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/References.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/References.feature index 3b4589e8a42..13a8e586444 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/References.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/References.feature @@ -22,13 +22,14 @@ Feature: Find and count references and their target nodes using the findReferenc type: integer dateProperty: type: DateTime + references: refs: - type: references properties: foo: type: string ref: - type: reference + constraints: + maxItems: 1 properties: foo: type: string diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/SiblingNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/SiblingNodes.feature index ea4e1dda130..a94e992bd83 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/SiblingNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/SiblingNodes.feature @@ -12,13 +12,14 @@ Feature: Find sibling nodes using the findPrecedingSiblingNodes and findSucceedi properties: text: type: string + references: refs: - type: references properties: foo: type: string ref: - type: reference + constraints: + maxItems: 1 properties: foo: type: string diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature index b9aac7a60d6..ed4672f8e85 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature @@ -14,13 +14,14 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la properties: text: type: string + references: refs: - type: references properties: foo: type: string ref: - type: reference + constraints: + maxItems: 1 properties: foo: type: string diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php index 6054ce5cc7c..b4e665910d4 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php @@ -20,6 +20,7 @@ use Neos\ContentRepository\Core\DimensionSpace\Exception\DimensionSpacePointNotFound; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\SerializedNodeReferences; use Neos\ContentRepository\Core\Feature\NodeVariation\Exception\DimensionSpacePointIsAlreadyOccupied; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyType; use Neos\ContentRepository\Core\NodeType\ConstraintCheck; @@ -46,6 +47,7 @@ use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeIsOfTypeRoot; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFound; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; +use Neos\ContentRepository\Core\SharedModel\Exception\PropertyCannotBeSet; use Neos\ContentRepository\Core\SharedModel\Exception\ReferenceCannotBeSet; use Neos\ContentRepository\Core\SharedModel\Exception\RootNodeAggregateDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Exception\RootNodeAggregateTypeIsAlreadyOccupied; @@ -196,20 +198,21 @@ protected function requireTetheredDescendantNodeTypesToNotBeOfTypeRoot(NodeType protected function requireNodeTypeToDeclareProperty(NodeTypeName $nodeTypeName, PropertyName $propertyName): void { $nodeType = $this->requireNodeType($nodeTypeName); - if (!isset($nodeType->getProperties()[$propertyName->value])) { + if (!$nodeType->hasProperty($propertyName->value)) { + throw PropertyCannotBeSet::becauseTheNodeTypeDoesNotDeclareIt( + $propertyName, + $nodeTypeName + ); } } - protected function requireNodeTypeToDeclareReference(NodeTypeName $nodeTypeName, ReferenceName $propertyName): void + protected function requireNodeTypeToDeclareReference(NodeTypeName $nodeTypeName, ReferenceName $referenceName): void { $nodeType = $this->requireNodeType($nodeTypeName); - if (isset($nodeType->getProperties()[$propertyName->value])) { - $propertyType = $nodeType->getPropertyType($propertyName->value); - if ($propertyType === 'reference' || $propertyType === 'references') { - return; - } + if ($nodeType->hasReference($referenceName->value)) { + return; } - throw ReferenceCannotBeSet::becauseTheNodeTypeDoesNotDeclareIt($propertyName, $nodeTypeName); + throw ReferenceCannotBeSet::becauseTheNodeTypeDoesNotDeclareIt($referenceName, $nodeTypeName); } protected function requireNodeTypeToAllowNodesOfTypeInReference( @@ -218,15 +221,10 @@ protected function requireNodeTypeToAllowNodesOfTypeInReference( NodeTypeName $nodeTypeNameInQuestion ): void { $nodeType = $this->requireNodeType($nodeTypeName); - $propertyDeclaration = $nodeType->getProperties()[$referenceName->value] ?? null; - if (is_null($propertyDeclaration)) { - throw ReferenceCannotBeSet::becauseTheNodeTypeDoesNotDeclareIt($referenceName, $nodeTypeName); - } - - $constraints = $propertyDeclaration['constraints']['nodeTypes'] ?? []; + $constraints = $nodeType->getReferences()[$referenceName->value]['constraints']['nodeTypes'] ?? []; if (!ConstraintCheck::create($constraints)->isNodeTypeAllowed($this->requireNodeType($nodeTypeNameInQuestion))) { - throw ReferenceCannotBeSet::becauseTheConstraintsAreNotMatched( + throw ReferenceCannotBeSet::becauseTheNodeTypeConstraintsAreNotMatched( $referenceName, $nodeTypeName, $nodeTypeNameInQuestion @@ -234,6 +232,24 @@ protected function requireNodeTypeToAllowNodesOfTypeInReference( } } + protected function requireNodeTypeToAllowNumberOfReferencesInReference(SerializedNodeReferences $nodeReferences, ReferenceName $referenceName, NodeTypeName $nodeTypeName): void + { + $nodeType = $this->requireNodeType($nodeTypeName); + + $maxItems = $nodeType->getReferences()[$referenceName->value]['constraints']['maxItems'] ?? null; + if ($maxItems === null) { + return; + } + + if ($maxItems < count($nodeReferences)) { + throw ReferenceCannotBeSet::becauseTheItemsCountConstraintsAreNotMatched( + $referenceName, + $nodeTypeName, + count($nodeReferences) + ); + } + } + /** * NodeType and NodeName must belong together to the same node, which is the to-be-checked one. * @@ -644,11 +660,11 @@ protected function validateReferenceProperties( $nodeType = $this->requireNodeType($nodeTypeName); foreach ($referenceProperties->values as $propertyName => $propertyValue) { - $referencePropertyConfig = $nodeType->getProperties()[$referenceName->value]['properties'][$propertyName] + $referencePropertyConfig = $nodeType->getReferences()[$referenceName->value]['properties'][$propertyName] ?? null; if (is_null($referencePropertyConfig)) { - throw ReferenceCannotBeSet::becauseTheItDoesNotDeclareAProperty( + throw ReferenceCannotBeSet::becauseTheReferenceDoesNotDeclareTheProperty( $referenceName, $nodeTypeName, PropertyName::fromString($propertyName) diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php index c2f1ff8a42a..b1000b3a34e 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php @@ -102,12 +102,7 @@ private function validateProperties(?PropertyValuesToWrite $propertyValues, Node $nodeType = $this->requireNodeType($nodeTypeName); foreach ($propertyValues->values as $propertyName => $propertyValue) { - if (!isset($nodeType->getProperties()[$propertyName])) { - throw PropertyCannotBeSet::becauseTheNodeTypeDoesNotDeclareIt( - PropertyName::fromString($propertyName), - $nodeTypeName - ); - } + $this->requireNodeTypeToDeclareProperty($nodeTypeName, PropertyName::fromString($propertyName)); $propertyType = PropertyType::fromNodeTypeDeclaration( $nodeType->getPropertyType($propertyName), PropertyName::fromString($propertyName), diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Dto/NodeReferencesToWrite.php b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Dto/NodeReferencesToWrite.php index fd6f36af88b..3fefefbde63 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Dto/NodeReferencesToWrite.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Dto/NodeReferencesToWrite.php @@ -26,7 +26,7 @@ * @implements \IteratorAggregate * @api used as part of commands */ -final readonly class NodeReferencesToWrite implements \IteratorAggregate, \JsonSerializable +final readonly class NodeReferencesToWrite implements \IteratorAggregate, \Countable, \JsonSerializable { /** * @var array @@ -58,6 +58,14 @@ public static function fromArray(array $values): self )); } + /** + * Unset all references for this reference name. + */ + public static function createEmpty(): self + { + return new self(); + } + public static function fromNodeAggregateIds(NodeAggregateIds $nodeAggregateIds): self { return new self(...array_map( @@ -90,4 +98,9 @@ public function jsonSerialize(): array { return $this->references; } + + public function count(): int + { + return count($this->references); + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Dto/SerializedNodeReferences.php b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Dto/SerializedNodeReferences.php index ecf9bf32f9e..4fbe1fb6737 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Dto/SerializedNodeReferences.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Dto/SerializedNodeReferences.php @@ -32,6 +32,13 @@ private function __construct(SerializedNodeReference ...$references) { + $existingTargets = []; + foreach ($references as $reference) { + if (isset($existingTargets[$reference->targetNodeAggregateId->value])) { + throw new \InvalidArgumentException(sprintf('Duplicate entry in references to write. Target "%s" already exists in collection.', $reference->targetNodeAggregateId->value), 1700150910); + } + $existingTargets[$reference->targetNodeAggregateId->value] = true; + } $this->references = $references; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/NodeReferencing.php b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/NodeReferencing.php index 7e82df8ef55..507e5c6bb6c 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/NodeReferencing.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/NodeReferencing.php @@ -115,6 +115,12 @@ private function handleSetSerializedNodeReferences( ); $this->requireNodeTypeToDeclareReference($sourceNodeAggregate->nodeTypeName, $command->referenceName); + $this->requireNodeTypeToAllowNumberOfReferencesInReference( + $command->references, + $command->referenceName, + $sourceNodeAggregate->nodeTypeName + ); + foreach ($command->references as $reference) { assert($reference instanceof SerializedNodeReference); $destinationNodeAggregate = $this->requireProjectedNodeAggregate( @@ -135,7 +141,7 @@ private function handleSetSerializedNodeReferences( } $sourceNodeType = $this->requireNodeType($sourceNodeAggregate->nodeTypeName); - $scopeDeclaration = $sourceNodeType->getProperties()[$command->referenceName->value]['scope'] ?? ''; + $scopeDeclaration = $sourceNodeType->getReferences()[$command->referenceName->value]['scope'] ?? ''; $scope = PropertyScope::tryFrom($scopeDeclaration) ?: PropertyScope::SCOPE_NODE; $affectedOrigins = $scope->resolveAffectedOrigins( diff --git a/Neos.ContentRepository.Core/Classes/Infrastructure/Property/PropertyConverter.php b/Neos.ContentRepository.Core/Classes/Infrastructure/Property/PropertyConverter.php index a7def6a3dd3..1857a3886a3 100644 --- a/Neos.ContentRepository.Core/Classes/Infrastructure/Property/PropertyConverter.php +++ b/Neos.ContentRepository.Core/Classes/Infrastructure/Property/PropertyConverter.php @@ -100,7 +100,7 @@ public function serializeReferencePropertyValues( $serializedPropertyValues = []; foreach ($propertyValuesToWrite->values as $propertyName => $propertyValue) { - $declaredType = $nodeType->getProperties()[$referenceName->value]['properties'][$propertyName]['type']; + $declaredType = $nodeType->getReferences()[$referenceName->value]['properties'][$propertyName]['type'] ?? 'string'; $serializedPropertyValues[$propertyName] = $this->serializePropertyValue( PropertyType::fromNodeTypeDeclaration( diff --git a/Neos.ContentRepository.Core/Classes/Infrastructure/Property/PropertyType.php b/Neos.ContentRepository.Core/Classes/Infrastructure/Property/PropertyType.php index 4cff2226dc7..bac9600942d 100644 --- a/Neos.ContentRepository.Core/Classes/Infrastructure/Property/PropertyType.php +++ b/Neos.ContentRepository.Core/Classes/Infrastructure/Property/PropertyType.php @@ -66,9 +66,6 @@ public static function fromNodeTypeDeclaration( PropertyName $propertyName, NodeTypeName $nodeTypeName ): self { - if ($declaration === 'reference' || $declaration === 'references') { - throw PropertyTypeIsInvalid::becauseItIsReference($propertyName, $nodeTypeName); - } $type = self::tryFromString($declaration); if (!$type) { throw PropertyTypeIsInvalid::becauseItIsUndefined($propertyName, $declaration, $nodeTypeName); @@ -78,9 +75,6 @@ public static function fromNodeTypeDeclaration( private static function tryFromString(string $declaration): ?self { - if ($declaration === 'reference' || $declaration === 'references') { - return null; - } if ($declaration === 'bool' || $declaration === 'boolean') { return self::bool(); } diff --git a/Neos.ContentRepository.Core/Classes/NodeType/NodeType.php b/Neos.ContentRepository.Core/Classes/NodeType/NodeType.php index e78c5b54e17..187e614a3bc 100644 --- a/Neos.ContentRepository.Core/Classes/NodeType/NodeType.php +++ b/Neos.ContentRepository.Core/Classes/NodeType/NodeType.php @@ -16,6 +16,7 @@ use Neos\ContentRepository\Core\NodeType\Exception\TetheredNodeNotConfigured; use Neos\ContentRepository\Core\SharedModel\Exception\InvalidNodeTypePostprocessorException; +use Neos\ContentRepository\Core\SharedModel\Exception\NodeConfigurationException; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\Utility\Arrays; use Neos\Utility\ObjectAccess; @@ -30,7 +31,7 @@ * * @api Note: The constructor is not part of the public API */ -class NodeType +final class NodeType { /** * Name of this node type. Example: "ContentRepository:Folder" @@ -77,7 +78,7 @@ class NodeType /** * @param NodeTypeName $name Name of the node type - * @param array $declaredSuperTypes Parent types of this node type + * @param array $declaredSuperTypes Parent types instances of this node type, if null it should be unset * @param array $configuration the configuration for this node type which is defined in the schema * @throws \InvalidArgumentException * @@ -90,15 +91,6 @@ public function __construct( private readonly NodeLabelGeneratorFactoryInterface $nodeLabelGeneratorFactory ) { $this->name = $name; - - foreach ($declaredSuperTypes as $type) { - if ($type !== null && !$type instanceof NodeType) { - throw new \InvalidArgumentException( - '$declaredSuperTypes must be an array of NodeType objects', - 1291300950 - ); - } - } $this->declaredSuperTypes = $declaredSuperTypes; if (isset($configuration['abstract']) && $configuration['abstract'] === true) { @@ -155,6 +147,40 @@ protected function buildFullConfiguration(): array $this->fullConfiguration['childNodes'] = $sorter->toArray(); } + $referencesConfiguration = $this->fullConfiguration['references'] ?? []; + foreach ($this->fullConfiguration['properties'] ?? [] as $propertyName => $propertyConfiguration) { + // assert that references and properties never declare a thing with the same name + if (isset($this->fullConfiguration['references'][$propertyName])) { + throw new NodeConfigurationException(sprintf('NodeType %s cannot declare "%s" as property and reference.', $this->name->value, $propertyName), 1708022344); + } + // migrate old property like references to references + $propertyType = $propertyConfiguration['type'] ?? null; + if ($propertyType !== 'reference' && $propertyType !== 'references') { + continue; + } + if (isset($propertyConfiguration['constraints']) || isset($propertyConfiguration['properties'])) { + // we don't allow the new syntax `constraints.maxItems` on legacy property-like reference-declarations + throw new NodeConfigurationException(sprintf( + 'Legacy property-like reference-declaration for "%s" does not allow new configuration `constraints` or `properties` in NodeType %s.' + . ' Please use the reference declaration syntax instead.', + $propertyName, + $this->name->value + ), 1708022344); + } + if ($propertyType === 'reference') { + unset($propertyConfiguration['type']); + $propertyConfiguration['constraints']['maxItems'] = 1; + $referencesConfiguration[$propertyName] = $propertyConfiguration; + unset($this->fullConfiguration['properties'][$propertyName]); + } + if ($propertyType === 'references') { + unset($propertyConfiguration['type']); + $referencesConfiguration[$propertyName] = $propertyConfiguration; + unset($this->fullConfiguration['properties'][$propertyName]); + } + } + $this->fullConfiguration['references'] = $referencesConfiguration; + return $this->fullConfiguration; } @@ -399,6 +425,31 @@ public function getProperties(): array return ($this->fullConfiguration['properties'] ?? []); } + /** + * Check if the property is configured in the schema. + */ + public function hasReference(string $referenceName): bool + { + $this->initialize(); + + return isset($this->fullConfiguration['references'][$referenceName]); + } + + /** + * Return the array with the defined references. The key is the reference name, + * the value the reference configuration. There are no guarantees on how the + * reference configuration looks like. + * + * @return array + * @api + */ + public function getReferences(): array + { + $this->initialize(); + + return ($this->fullConfiguration['references'] ?? []); + } + /** * Check if the property is configured in the schema. */ @@ -421,7 +472,7 @@ public function getPropertyType(string $propertyName): string if (!$this->hasProperty($propertyName)) { throw new \InvalidArgumentException( sprintf('NodeType schema has no property "%s" configured for the NodeType "%s". Cannot read its type.', $propertyName, $this->name->value), - 1695062252040 + 1708025421 ); } diff --git a/Neos.ContentRepository.Core/Classes/NodeType/NodeTypeManager.php b/Neos.ContentRepository.Core/Classes/NodeType/NodeTypeManager.php index 6962ceaa9e0..4c98cdf9383 100644 --- a/Neos.ContentRepository.Core/Classes/NodeType/NodeTypeManager.php +++ b/Neos.ContentRepository.Core/Classes/NodeType/NodeTypeManager.php @@ -332,11 +332,11 @@ private function loadNodeType(string $nodeTypeName, array &$completeNodeTypeConf * * @param array $superTypesConfiguration * @param array $completeNodeTypeConfiguration - * @return array + * @return array */ private function evaluateSuperTypesConfiguration( array $superTypesConfiguration, - array &$completeNodeTypeConfiguration + array $completeNodeTypeConfiguration ): array { $superTypes = []; foreach ($superTypesConfiguration as $superTypeName => $enabled) { diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php index e1acfb67fc2..0d1f7d3f8f5 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php @@ -144,7 +144,6 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, Filter\FindSu /** * Find all "outgoing" references of a given node that match the specified $filter * - * A reference is a node property of type "reference" or "references" * Because each reference has a name and can contain properties itself, this method does not return the target nodes * directly, but a collection of references {@see References}. * The corresponding nodes can be retrieved via {@see References::getNodes()} diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/References.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/References.php index ea15ca2a268..e071bd9ffe1 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/References.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/References.php @@ -17,8 +17,6 @@ /** * An immutable, 0-indexed, type-safe collection of Reference objects * - * A reference is a node property of type "reference" or "references" - * * Each reference describes the edge with its properties to another node. * * - references: @@ -31,7 +29,7 @@ * * The properties {@see Reference::$properties} are declared directly on the reference, and can provide information how one node is linked to another. * - * If multiple "outgoing" references are allowed via type "references", this collection will return multiple references with the same name {@see Reference::$name}. + * This collection might return multiple references with the same name {@see Reference::$name} if multiple "outgoing" references were set. * * @implements \IteratorAggregate * @implements \ArrayAccess diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Exception/PropertyTypeIsInvalid.php b/Neos.ContentRepository.Core/Classes/SharedModel/Exception/PropertyTypeIsInvalid.php index 8b5c241e063..c62b6e64dbd 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Exception/PropertyTypeIsInvalid.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Exception/PropertyTypeIsInvalid.php @@ -24,15 +24,6 @@ */ final class PropertyTypeIsInvalid extends \DomainException { - public static function becauseItIsReference(PropertyName $propertyName, NodeTypeName $nodeTypeName): self - { - return new self( - 'Given property "' . $propertyName->value . '" is declared as "reference" in node type "' - . $nodeTypeName->value . '" and must be treated as such.', - 1630063201 - ); - } - public static function becauseItIsUndefined( PropertyName $propertyName, string $declaredType, diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Exception/ReferenceCannotBeSet.php b/Neos.ContentRepository.Core/Classes/SharedModel/Exception/ReferenceCannotBeSet.php index 4bc40a33e0e..46c6ca54780 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Exception/ReferenceCannotBeSet.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Exception/ReferenceCannotBeSet.php @@ -36,7 +36,7 @@ public static function becauseTheNodeTypeDoesNotDeclareIt( ); } - public static function becauseTheConstraintsAreNotMatched( + public static function becauseTheNodeTypeConstraintsAreNotMatched( ReferenceName $referenceName, NodeTypeName $nodeTypeName, NodeTypeName $nameOfAttemptedType @@ -48,7 +48,19 @@ public static function becauseTheConstraintsAreNotMatched( ); } - public static function becauseTheItDoesNotDeclareAProperty( + public static function becauseTheItemsCountConstraintsAreNotMatched( + ReferenceName $referenceName, + NodeTypeName $nodeTypeName, + int $numberOfAttemptedReferencesToWrite + ): self { + return new self( + 'Reference "' . $referenceName->value . '" cannot be set for node type "' + . $nodeTypeName->value . '" because the constraints do not allow to set ' . $numberOfAttemptedReferencesToWrite . ' references', + 1700150156 + ); + } + + public static function becauseTheReferenceDoesNotDeclareTheProperty( ReferenceName $referenceName, NodeTypeName $nodeTypeName, PropertyName $propertyName diff --git a/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeManagerTest.php b/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeManagerTest.php index 486e3e4d770..fc5de248c5b 100644 --- a/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeManagerTest.php +++ b/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeManagerTest.php @@ -397,7 +397,7 @@ public function allInheritedNodeTypePropertiesCannotBeUnset(): void /** * @test */ - public function anInheritedNodeTypePropertyCannotBeSetToEmptyArray(): void + public function anInheritedNodeTypePropertyCanBeOverruledWithEmptyArray(): void { $nodeTypesFixture = [ 'Neos.ContentRepository.Testing:Base' => [ diff --git a/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeTest.php b/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeTest.php index 3367384c538..45a346cd05e 100644 --- a/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeTest.php +++ b/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeTest.php @@ -15,6 +15,7 @@ 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 PHPUnit\Framework\TestCase; /** @@ -135,20 +136,45 @@ public function aNodeTypeHasAName() /** * @test */ - public function setDeclaredSuperTypesExpectsAnArrayOfNodeTypesAsKeys() + public function aNodeTypeMustHaveDistinctNamesForPropertiesReferences() { - $this->expectException(\InvalidArgumentException::class); - new NodeType(NodeTypeName::fromString('ContentRepository:Folder'), ['foo' => true], [], new DefaultNodeLabelGeneratorFactory() - ); + $nodeType = new NodeType(NodeTypeName::fromString('ContentRepository:Invalid'), [], [ + 'properties' => [ + 'foo' => [ + 'type' => 'string', + ] + ], + 'references' => [ + 'foo' => [] + ] + ], new DefaultNodeLabelGeneratorFactory()); + $this->expectException(NodeConfigurationException::class); + $this->expectExceptionCode(1708022344); + // initialize the node type + $nodeType->getFullConfiguration(); } /** * @test */ - public function setDeclaredSuperTypesAcceptsAnArrayOfNodeTypes() + public function aNodeTypeMustHaveDistinctNamesForPropertiesReferencesInInheritance() { - $this->expectException(\InvalidArgumentException::class); - new NodeType(NodeTypeName::fromString('ContentRepository:Folder'), ['foo'], [], new DefaultNodeLabelGeneratorFactory()); + $superNodeType = new NodeType(NodeTypeName::fromString('ContentRepository:Super'), [], [ + 'properties' => [ + 'foo' => [ + 'type' => 'string', + ] + ] + ], new DefaultNodeLabelGeneratorFactory()); + $nodeType = new NodeType(NodeTypeName::fromString('ContentRepository:Invalid'), ['ContentRepository:Super' => $superNodeType], [ + 'references' => [ + 'foo' => [] + ] + ], new DefaultNodeLabelGeneratorFactory()); + $this->expectException(NodeConfigurationException::class); + $this->expectExceptionCode(1708022344); + // initialize the node type + $nodeType->getFullConfiguration(); } /** @@ -365,6 +391,212 @@ public function superTypesRemovedByInheritanceCanBeAddedAgain() self::assertSame($expectedProperties, $nodeType->getProperties()); } + /** + * @test + */ + public function propertyDeclaration() + { + $nodeType = new NodeType(NodeTypeName::fromString('ContentRepository:Node'), [], [ + 'properties' => [ + 'someProperty' => [ + 'type' => 'bool', + 'defaultValue' => false + ] + ] + ], new DefaultNodeLabelGeneratorFactory()); + self::assertTrue($nodeType->hasProperty('someProperty')); + self::assertFalse($nodeType->hasReference('someProperty')); + self::assertSame('bool', $nodeType->getPropertyType('someProperty')); + self::assertEmpty($nodeType->getReferences()); + self::assertNull($nodeType->getConfiguration('references.someProperty')); + self::assertNotNull($nodeType->getConfiguration('properties.someProperty')); + self::assertSame(['someProperty' => false], $nodeType->getDefaultValuesForProperties()); + self::assertSame( + [ + 'someProperty' => [ + 'type' => 'bool', + 'defaultValue' => false + ] + ], + $nodeType->getProperties() + ); + } + + /** + * @test + */ + public function getPropertyTypeThrowsOnInvalidProperty() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(1708025421); + $nodeType = new NodeType(NodeTypeName::fromString('ContentRepository:Node'), [], [], new DefaultNodeLabelGeneratorFactory()); + $nodeType->getPropertyType('nonExistent'); + self::assertSame('string', $nodeType->getPropertyType('nonExistent')); + } + + /** + * @test + */ + public function getPropertyTypeFallback() + { + $nodeType = new NodeType(NodeTypeName::fromString('ContentRepository:Node'), [], [ + 'properties' => [ + 'someProperty' => [] + ] + ], new DefaultNodeLabelGeneratorFactory()); + self::assertSame('string', $nodeType->getPropertyType('someProperty')); + } + + /** + * @test + */ + public function getDefaultValuesForPropertiesIgnoresNullAndUnset() + { + $nodeType = new NodeType(NodeTypeName::fromString('ContentRepository:Node'), [], [ + 'properties' => [ + 'someProperty' => [ + 'type' => 'string', + 'defaultValue' => 'lol' + ], + 'otherProperty' => [ + 'type' => 'string', + 'defaultValue' => null + ], + 'thirdProperty' => [ + 'type' => 'string' + ] + ] + ], new DefaultNodeLabelGeneratorFactory()); + self::assertSame(['someProperty' => 'lol'], $nodeType->getDefaultValuesForProperties()); + } + + /** + * @test + */ + public function referencesDeclaration() + { + $nodeType = new NodeType(NodeTypeName::fromString('ContentRepository:Node'), [], [ + 'references' => [ + 'someReferences' => [] + ] + ], new DefaultNodeLabelGeneratorFactory()); + self::assertFalse($nodeType->hasProperty('someReferences')); + self::assertTrue($nodeType->hasReference('someReferences')); + self::assertThrows(fn() => $nodeType->getPropertyType('someReferences'), \InvalidArgumentException::class); + self::assertEmpty($nodeType->getProperties()); + self::assertEmpty($nodeType->getDefaultValuesForProperties()); + self::assertNull($nodeType->getConfiguration('properties.someReferences')); + self::assertNotNull($nodeType->getConfiguration('references.someReferences')); + self::assertSame( + [ + 'someReferences' => [] + ], + $nodeType->getReferences() + ); + } + + /** + * @test + */ + public function legacyPropertyReferenceDeclaration() + { + $nodeType = new NodeType(NodeTypeName::fromString('ContentRepository:Node'), [], [ + 'properties' => [ + 'referenceProperty' => [ + 'type' => 'reference', + ] + ] + ], new DefaultNodeLabelGeneratorFactory()); + // will be available as _real_ reference + self::assertFalse($nodeType->hasProperty('referenceProperty')); + self::assertTrue($nodeType->hasReference('referenceProperty')); + self::assertThrows(fn() => $nodeType->getPropertyType('referenceProperty'), \InvalidArgumentException::class); + self::assertEmpty($nodeType->getProperties()); + self::assertEmpty($nodeType->getDefaultValuesForProperties()); + self::assertNull($nodeType->getConfiguration('properties.referenceProperty')); + self::assertNotNull($nodeType->getConfiguration('references.referenceProperty')); + self::assertSame( + [ + 'referenceProperty' => [ + 'constraints' => [ + 'maxItems' => 1 + ] + ] + ], + $nodeType->getReferences() + ); + } + + /** + * @test + */ + public function legacyPropertyReferencesDeclaration() + { + $nodeType = new NodeType(NodeTypeName::fromString('ContentRepository:Node'), [], [ + 'properties' => [ + 'referencesProperty' => [ + 'type' => 'references', + ] + ] + ], new DefaultNodeLabelGeneratorFactory()); + // will be available as _real_ reference + self::assertFalse($nodeType->hasProperty('referencesProperty')); + self::assertTrue($nodeType->hasReference('referencesProperty')); + self::assertThrows(fn() => $nodeType->getPropertyType('referencesProperty'), \InvalidArgumentException::class); + self::assertEmpty($nodeType->getProperties()); + self::assertEmpty($nodeType->getDefaultValuesForProperties()); + self::assertNull($nodeType->getConfiguration('properties.referencesProperty')); + self::assertNotNull($nodeType->getConfiguration('references.referencesProperty')); + self::assertSame( + [ + 'referencesProperty' => [] + ], + $nodeType->getReferences() + ); + } + + /** + * @test + */ + public function legacyPropertyReferencesDeclarationMustNotUseConstraintFeatures() + { + $nodeType = new NodeType(NodeTypeName::fromString('ContentRepository:Node'), [], [ + 'properties' => [ + 'referencesProperty' => [ + 'type' => 'references', + 'constraints' => [ + 'maxItems' => 1 + ], + ] + ] + ], new DefaultNodeLabelGeneratorFactory()); + $this->expectException(NodeConfigurationException::class); + $this->expectExceptionCode(1708022344); + $nodeType->getReferences(); + } + + /** + * @test + */ + public function legacyPropertyReferencesDeclarationMustNotUsePropertiesFeatures() + { + $nodeType = new NodeType(NodeTypeName::fromString('ContentRepository:Node'), [], [ + 'properties' => [ + 'referencesProperty' => [ + 'type' => 'references', + 'properties' => [ + 'text' => [ + 'type' => 'string' + ] + ], + ] + ] + ], new DefaultNodeLabelGeneratorFactory()); + $this->expectException(NodeConfigurationException::class); + $this->expectExceptionCode(1708022344); + $nodeType->getReferences(); + } + /** * Return a nodetype built from the nodeTypesFixture */ @@ -376,6 +608,7 @@ protected function getNodeType(string $nodeTypeName): ?NodeType $configuration = $this->nodeTypesFixture[$nodeTypeName]; $declaredSuperTypes = []; + // duplicated from the node type manager if (isset($configuration['superTypes']) && is_array($configuration['superTypes'])) { foreach ($configuration['superTypes'] as $superTypeName => $enabled) { $declaredSuperTypes[$superTypeName] = $enabled === true ? $this->getNodeType($superTypeName) : null; @@ -389,4 +622,15 @@ protected function getNodeType(string $nodeTypeName): ?NodeType new DefaultNodeLabelGeneratorFactory() ); } + + private static function assertThrows(callable $fn, string $exceptionClassName): void + { + try { + $fn(); + } catch (\Throwable $e) { + self::assertInstanceOf($exceptionClassName, $e); + return; + } + self::fail('$fn should throw.'); + } } diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php index 2e2aa35aad9..a621f5309e2 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php @@ -346,14 +346,7 @@ public function extractPropertyValuesAndReferences(array $nodeDataRow, NodeType } foreach ($decodedProperties as $propertyName => $propertyValue) { - try { - $type = $nodeType->getPropertyType($propertyName); - } catch (\InvalidArgumentException $exception) { - $this->dispatch(Severity::WARNING, 'Skipped node data processing for the property "%s". The property name is not part of the NodeType schema for the NodeType "%s". (Node: %s)', $propertyName, $nodeType->name->value, $nodeDataRow['identifier']); - continue; - } - - if ($type === 'reference' || $type === 'references') { + if ($nodeType->hasReference($propertyName)) { if (!empty($propertyValue)) { if (!is_array($propertyValue)) { $propertyValue = [$propertyValue]; @@ -363,6 +356,11 @@ public function extractPropertyValuesAndReferences(array $nodeDataRow, NodeType continue; } + if (!$nodeType->hasProperty($propertyName)) { + $this->dispatch(Severity::WARNING, 'Skipped node data processing for the property "%s". The property name is not part of the NodeType schema for the NodeType "%s". (Node: %s)', $propertyName, $nodeType->name->value, $nodeDataRow['identifier']); + continue; + } + $type = $nodeType->getPropertyType($propertyName); // In the old `Node`, we call the property mapper to convert the returned properties from NodeData; // so we need to do the same here. try { diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PropertyOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PropertyOperation.php index 1a87d02de6f..fe612a2cd5f 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PropertyOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PropertyOperation.php @@ -20,6 +20,7 @@ use Neos\Eel\FlowQuery\FlowQueryException; use Neos\Eel\FlowQuery\Operations\AbstractOperation; use Neos\Flow\Annotations as Flow; +use Neos\Neos\Utility\NodeTypeWithFallbackProvider; use Neos\Utility\ObjectAccess; /** @@ -56,6 +57,8 @@ class PropertyOperation extends AbstractOperation */ protected $contentRepositoryRegistry; + use NodeTypeWithFallbackProvider; + /** * {@inheritdoc} * @@ -110,26 +113,23 @@ public function evaluate(FlowQuery $flowQuery, array $arguments): mixed return ObjectAccess::getPropertyPath($element, substr($propertyName, 1)); } - $propertyType = $element->nodeType->hasProperty($propertyName) - ? $element->nodeType->getPropertyType($propertyName) - : null; - - if ($propertyType === 'reference') { + if ($this->getNodeType($element)->hasReference($propertyName)) { + // legacy access layer for references $subgraph = $this->contentRepositoryRegistry->subgraphForNode($element); - return ( - $subgraph->findReferences( - $element->nodeAggregateId, - FindReferencesFilter::create(referenceName: $propertyName) - )[0] ?? null - )?->node; - } - - if ($propertyType === 'references') { - $subgraph = $this->contentRepositoryRegistry->subgraphForNode($element); - return $subgraph->findReferences( + $references = $subgraph->findReferences( $element->nodeAggregateId, FindReferencesFilter::create(referenceName: $propertyName) )->getNodes(); + + $maxItems = $this->getNodeType($element)->getReferences()[$propertyName]['constraints']['maxItems'] ?? null; + if ($maxItems === 1) { + // legacy layer references with only one item like the previous `type: reference` + // (the node type transforms that to constraints.maxItems = 1) + // users still expect the property operation to return a single node instead of an array. + return $references->first(); + } + + return $references; } return $element->getProperty($propertyName); diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index 5082190b726..114a73ce42f 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -158,9 +158,10 @@ public function theLastCommandShouldHaveThrown(string $shortExceptionName, ?int Assert::assertSame($shortExceptionName, $lastCommandExceptionShortName, sprintf('Actual exception: %s (%s): %s', get_class($this->lastCommandException), $this->lastCommandException->getCode(), $this->lastCommandException->getMessage())); if (!is_null($expectedCode)) { Assert::assertSame($expectedCode, $this->lastCommandException->getCode(), sprintf( - 'Expected exception code %s, got exception code %s instead', + 'Expected exception code %s, got exception code %s instead; Message: %s', $expectedCode, - $this->lastCommandException->getCode() + $this->lastCommandException->getCode(), + $this->lastCommandException->getMessage() )); } } diff --git a/Neos.Neos/Classes/Controller/Service/NodesController.php b/Neos.Neos/Classes/Controller/Service/NodesController.php index 0481e8b7c53..d8db9a2fcc7 100644 --- a/Neos.Neos/Classes/Controller/Service/NodesController.php +++ b/Neos.Neos/Classes/Controller/Service/NodesController.php @@ -230,6 +230,7 @@ public function showAction(string $identifier, string $workspaceName = 'live', a $this->throwStatus(404); } + // @todo illegal dependency direction. Neos Neos has no business calling the ui $convertedNodeProperties = $this->nodePropertyConverterService->getPropertiesArray($node); array_walk($convertedNodeProperties, function (&$value) { if (is_array($value)) { diff --git a/Neos.Neos/Classes/NodeTypePostprocessor/DefaultPropertyEditorPostprocessor.php b/Neos.Neos/Classes/NodeTypePostprocessor/DefaultPropertyEditorPostprocessor.php index 8b5c175ad45..45fc93ca39f 100644 --- a/Neos.Neos/Classes/NodeTypePostprocessor/DefaultPropertyEditorPostprocessor.php +++ b/Neos.Neos/Classes/NodeTypePostprocessor/DefaultPropertyEditorPostprocessor.php @@ -46,6 +46,31 @@ class DefaultPropertyEditorPostprocessor implements NodeTypePostprocessorInterfa public function process(NodeType $nodeType, array &$configuration, array $options): void { $nodeTypeName = $nodeType->name->value; + + foreach ($configuration['references'] as $referenceName => &$referenceConfiguration) { + if (!isset($referenceConfiguration['ui']['inspector'])) { + // we presume that these are properties wich are not shown + continue; + } + + $editor = $referenceConfiguration['ui']['inspector']['editor'] ?? null; + + if (!$editor) { + $maxAllowedItems = $referenceConfiguration['constraints']['maxItems'] ?? null; + $editor = $maxAllowedItems === 1 + ? ($this->dataTypesDefaultConfiguration['reference']['editor'] ?? 'Neos.Neos/Inspector/Editors/ReferenceEditor') + : ($this->dataTypesDefaultConfiguration['references']['editor'] ?? 'Neos.Neos/Inspector/Editors/ReferencesEditor'); + } + + $mergedInspectorConfiguration = $this->editorDefaultConfiguration[$editor] ?? []; + $mergedInspectorConfiguration = Arrays::arrayMergeRecursiveOverrule( + $mergedInspectorConfiguration, + $referenceConfiguration['ui']['inspector'] + ); + $referenceConfiguration['ui']['inspector'] = $mergedInspectorConfiguration; + $referenceConfiguration['ui']['inspector']['editor'] = $editor; + } + if (isset($configuration['properties']) && is_array($configuration['properties'])) { foreach ($configuration['properties'] as $propertyName => &$propertyConfiguration) { if (!isset($propertyConfiguration['type'])) { @@ -55,6 +80,7 @@ public function process(NodeType $nodeType, array &$configuration, array $option $type = $propertyConfiguration['type']; if (!isset($propertyConfiguration['ui']['inspector'])) { + // we presume that these are properties wich are not shown continue; } diff --git a/Neos.Neos/Classes/Service/Mapping/NodeReferenceConverter.php b/Neos.Neos/Classes/Service/Mapping/NodeReferenceConverter.php deleted file mode 100644 index aa4625ce7de..00000000000 --- a/Neos.Neos/Classes/Service/Mapping/NodeReferenceConverter.php +++ /dev/null @@ -1,79 +0,0 @@ - - * @api - */ - protected $sourceTypes = [Node::class, 'array']; - - /** - * The target type this converter can convert to. - * - * @var string - * @api - */ - protected $targetType = 'string'; - - /** - * The priority for this converter. - * - * @var integer - * @api - */ - protected $priority = 0; - - /** - * {@inheritdoc} - * - * @param Node|array $source - * @param string $targetType - * @param array $convertedChildProperties - * @param PropertyMappingConfigurationInterface $configuration - * @return string|array the target type - */ - public function convertFrom( - $source, - $targetType, - array $convertedChildProperties = [], - PropertyMappingConfigurationInterface $configuration = null - ) { - if (is_array($source)) { - $result = []; - foreach ($source as $node) { - $result[] = $node->nodeAggregateId->value; - } - } else { - $result = $source->nodeAggregateId->value; - } - - return $result; - } -} diff --git a/Neos.Neos/Classes/Service/Mapping/NodeTypeStringConverter.php b/Neos.Neos/Classes/Service/Mapping/NodeTypeStringConverter.php index 610a6d5b934..247ae5e9132 100644 --- a/Neos.Neos/Classes/Service/Mapping/NodeTypeStringConverter.php +++ b/Neos.Neos/Classes/Service/Mapping/NodeTypeStringConverter.php @@ -20,8 +20,8 @@ use Neos\Flow\Property\TypeConverter\AbstractTypeConverter; /** - * Convert a boolean to a JavaScript compatible string representation. - * + * @internal + * @deprecated todo still used? * @Flow\Scope("singleton") */ class NodeTypeStringConverter extends AbstractTypeConverter diff --git a/Neos.Neos/Classes/Service/NodeTypeSchemaBuilder.php b/Neos.Neos/Classes/Service/NodeTypeSchemaBuilder.php index ae8aeec7256..9e1d623a497 100644 --- a/Neos.Neos/Classes/Service/NodeTypeSchemaBuilder.php +++ b/Neos.Neos/Classes/Service/NodeTypeSchemaBuilder.php @@ -66,6 +66,11 @@ public function generateNodeTypeSchema() foreach ($nodeTypes as $nodeTypeName => $nodeType) { if ($nodeType->isAbstract() === false) { $configuration = $nodeType->getFullConfiguration(); + $configuration['properties'] = array_merge( + $configuration['properties'] ?? [], + $configuration['references'] ?? [], + ); + unset($configuration['references']); $schema['nodeTypes'][$nodeTypeName] = $configuration; $schema['nodeTypes'][$nodeTypeName]['label'] = $nodeType->getLabel(); } diff --git a/Neos.Neos/Configuration/Settings.yaml b/Neos.Neos/Configuration/Settings.yaml index 5ad7785a511..11ca11829ba 100755 --- a/Neos.Neos/Configuration/Settings.yaml +++ b/Neos.Neos/Configuration/Settings.yaml @@ -230,11 +230,11 @@ Neos: editor: Neos.Neos/Inspector/Editors/DateTimeEditor editorOptions: format: d-m-Y + # special types uses for NodeType references to wire the "editor" + # singular "reference" will be used if constraints.maxItems is set to 1 reference: - typeConverter: Neos\Neos\Service\Mapping\NodeReferenceConverter editor: Neos.Neos/Inspector/Editors/ReferenceEditor references: - typeConverter: Neos\Neos\Service\Mapping\NodeReferenceConverter editor: Neos.Neos/Inspector/Editors/ReferencesEditor editors: Neos.Neos/Inspector/Editors/CodeEditor: diff --git a/Neos.Neos/Tests/Unit/NodeTypePostprocessor/DefaultPropertyEditorPostprocessorTest.php b/Neos.Neos/Tests/Unit/NodeTypePostprocessor/DefaultPropertyEditorPostprocessorTest.php index cc0123cc8dd..dd2a83b674a 100644 --- a/Neos.Neos/Tests/Unit/NodeTypePostprocessor/DefaultPropertyEditorPostprocessorTest.php +++ b/Neos.Neos/Tests/Unit/NodeTypePostprocessor/DefaultPropertyEditorPostprocessorTest.php @@ -17,25 +17,77 @@ use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\Flow\Tests\UnitTestCase; use Neos\Neos\NodeTypePostprocessor\DefaultPropertyEditorPostprocessor; +use Symfony\Component\Yaml\Yaml; /** * Testcase for the DefaultPropertyEditorPostprocessor */ class DefaultPropertyEditorPostprocessorTest extends UnitTestCase { - private function processConfiguration(array $configuration, array $dataTypesDefaultConfiguration, array $editorDefaultConfiguration): array + public function referenceExamples(): iterable { - $postprocessor = new DefaultPropertyEditorPostprocessor(); - $this->inject($postprocessor, 'dataTypesDefaultConfiguration', $dataTypesDefaultConfiguration); - $this->inject($postprocessor, 'editorDefaultConfiguration', $editorDefaultConfiguration); - $mockNodeType = new NodeType( - NodeTypeName::fromString('Some.NodeType:Name'), - [], - [], - new DefaultNodeLabelGeneratorFactory() - ); - $postprocessor->process($mockNodeType, $configuration, []); - return $configuration; + yield 'multiple references' => [ + 'nodeTypeDefinition' => <<<'YAML' + references: + someReferences: + ui: + inspector: + group: 'foo' + YAML, + 'expected' => <<<'YAML' + references: + someReferences: + ui: + inspector: + group: 'foo' + editor: ReferencesEditor + YAML + ]; + + yield 'singular reference' => [ + 'nodeTypeDefinition' => <<<'YAML' + references: + someReference: + constraints: + maxItems: 1 + ui: + inspector: + group: 'foo' + YAML, + 'expected' => <<<'YAML' + references: + someReference: + constraints: + maxItems: 1 + ui: + inspector: + editor: SingularReferenceEditor + group: 'foo' + YAML + ]; + } + + /** + * @test + * @dataProvider referenceExamples + */ + public function processExamples(string $nodeTypeDefinition, string $expectedResult) + { + $configuration = array_merge(['references' => [], 'properties' => []], Yaml::parse($nodeTypeDefinition)); + + $dataTypesDefaultConfiguration = [ + 'reference' => [ + 'editor' => 'SingularReferenceEditor', + ], + 'references' => [ + 'editor' => 'ReferencesEditor', + ], + ]; + + $editorDefaultConfiguration = []; + + $actualResult = $this->processConfiguration($configuration, $dataTypesDefaultConfiguration, $editorDefaultConfiguration); + self::assertEquals(array_merge(['references' => [], 'properties' => []], Yaml::parse($expectedResult)), $actualResult); } /** @@ -44,6 +96,7 @@ private function processConfiguration(array $configuration, array $dataTypesDefa public function processConvertsPropertyConfiguration(): void { $configuration = [ + 'references' => [], 'properties' => [ 'propertyWithoutType' => [ 'ui' => [ @@ -154,6 +207,7 @@ public function processConvertsPropertyConfiguration(): void ]; $expectedResult = [ + 'references' => [], 'properties' => [ 'propertyWithoutType' => [ 'ui' => [ @@ -268,6 +322,7 @@ public function processThrowsExceptionIfNoPropertyEditorCanBeResolved(): void $this->expectException(\Neos\Neos\Exception::class); $configuration = [ + 'references' => [], 'properties' => [ 'someProperty' => [ 'type' => 'string', @@ -280,4 +335,19 @@ public function processThrowsExceptionIfNoPropertyEditorCanBeResolved(): void ]; $this->processConfiguration($configuration, $dataTypesDefaultConfiguration, []); } + + private function processConfiguration(array $configuration, array $dataTypesDefaultConfiguration, array $editorDefaultConfiguration): array + { + $postprocessor = new DefaultPropertyEditorPostprocessor(); + $this->inject($postprocessor, 'dataTypesDefaultConfiguration', $dataTypesDefaultConfiguration); + $this->inject($postprocessor, 'editorDefaultConfiguration', $editorDefaultConfiguration); + $mockNodeType = new NodeType( + NodeTypeName::fromString('Some.NodeType:Name'), + [], + [], + new DefaultNodeLabelGeneratorFactory() + ); + $postprocessor->process($mockNodeType, $configuration, []); + return $configuration; + } }