Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: FEATURE: Overhaul NodeTypeManager #4999

Draft
wants to merge 13 commits into
base: 9.0
Choose a base branch
from
Draft
Expand Up @@ -15,6 +15,7 @@
namespace Neos\ContentRepository\BehavioralTests\TestSuite\Behavior;

use Behat\Gherkin\Node\PyStringNode;
use Neos\ContentRepository\Core\NodeType\ClosureNodeTypeProvider;
use Neos\ContentRepository\Core\NodeType\NodeTypeManager;
use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId;
use Neos\ContentRepositoryRegistry\Factory\NodeTypeManager\NodeTypeManagerFactoryInterface;
Expand All @@ -41,7 +42,9 @@ public function build(ContentRepositoryId $contentRepositoryId, array $options):
public static function initializeWithPyStringNode(PyStringNode $nodeTypesToUse): void
{
self::$nodeTypesToUse = new NodeTypeManager(
fn (): array => Yaml::parse($nodeTypesToUse->getRaw()) ?? []
new ClosureNodeTypeProvider(
fn (): array => Yaml::parse($nodeTypesToUse->getRaw()) ?? [],
)
);
}

Expand Down
Expand Up @@ -31,6 +31,7 @@
use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace;
use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace;
use Neos\ContentRepository\Core\NodeType\ClosureNodeTypeProvider;
use Neos\ContentRepository\Core\NodeType\NodeTypeManager;
use Neos\ContentRepository\Core\NodeType\NodeTypeName;
use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId;
Expand Down Expand Up @@ -78,16 +79,18 @@ public function getContentDimensionsOrderedByPriority(): array
};

GherkinPyStringNodeBasedNodeTypeManagerFactory::$nodeTypesToUse = new NodeTypeManager(
fn (): array => [
'Neos.ContentRepository:Root' => [],
'Neos.ContentRepository.Testing:Document' => [
'properties' => [
'title' => [
'type' => 'string'
new ClosureNodeTypeProvider(
fn (): array => [
'Neos.ContentRepository:Root' => [],
'Neos.ContentRepository.Testing:Document' => [
'properties' => [
'title' => [
'type' => 'string'
]
]
]
]
]
],
)
);
$this->contentRepositoryRegistry = $this->objectManager->get(ContentRepositoryRegistry::class);

Expand Down
Expand Up @@ -23,7 +23,6 @@
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;
use Neos\ContentRepository\Core\NodeType\NodeType;
use Neos\ContentRepository\Core\NodeType\NodeTypeManager;
use Neos\ContentRepository\Core\NodeType\NodeTypeName;
Expand Down Expand Up @@ -66,6 +65,7 @@
use Neos\EventStore\Model\EventStream\ExpectedVersion;

/**
* TODO Remove
* @internal implementation details of command handlers
*/
trait ConstraintChecks
Expand Down Expand Up @@ -124,13 +124,6 @@ protected function requireNodeType(NodeTypeName $nodeTypeName): NodeType
);
}

protected function requireNodeTypeToNotBeAbstract(NodeType $nodeType): void
{
if ($nodeType->isAbstract()) {
throw NodeTypeIsAbstract::butWasNotSupposedToBe($nodeType->name);
}
}

/**
* @param NodeType $nodeType
* @throws NodeTypeIsNotOfTypeRoot
Expand Down Expand Up @@ -213,10 +206,9 @@ protected function requireNodeTypeToDeclareProperty(NodeTypeName $nodeTypeName,
protected function requireNodeTypeToDeclareReference(NodeTypeName $nodeTypeName, ReferenceName $referenceName): void
{
$nodeType = $this->requireNodeType($nodeTypeName);
if ($nodeType->hasReference($referenceName->value)) {
return;
if (!$nodeType->referenceDefinitions->contain($referenceName)) {
throw ReferenceCannotBeSet::becauseTheNodeTypeDoesNotDeclareIt($referenceName, $nodeTypeName);
}
throw ReferenceCannotBeSet::becauseTheNodeTypeDoesNotDeclareIt($referenceName, $nodeTypeName);
}

protected function requireNodeTypeNotToDeclareTetheredChildNodeName(NodeTypeName $nodeTypeName, NodeName $nodeName): void
Expand All @@ -236,9 +228,14 @@ protected function requireNodeTypeToAllowNodesOfTypeInReference(
NodeTypeName $nodeTypeNameInQuestion
): void {
$nodeType = $this->requireNodeType($nodeTypeName);
$constraints = $nodeType->getReferences()[$referenceName->value]['constraints']['nodeTypes'] ?? [];

if (!ConstraintCheck::create($constraints)->isNodeTypeAllowed($this->requireNodeType($nodeTypeNameInQuestion))) {
$referenceDefinition = $nodeType->referenceDefinitions->get($referenceName);
if ($referenceDefinition === null) {
throw ReferenceCannotBeSet::becauseTheNodeTypeDoesNotDeclareIt(
$referenceName,
$nodeTypeName,
);
}
if (!$referenceDefinition->nodeTypeConstraints->isNodeTypeAllowed($nodeTypeNameInQuestion)) {
throw ReferenceCannotBeSet::becauseTheNodeTypeConstraintsAreNotMatched(
$referenceName,
$nodeTypeName,
Expand All @@ -250,8 +247,15 @@ protected function requireNodeTypeToAllowNodesOfTypeInReference(
protected function requireNodeTypeToAllowNumberOfReferencesInReference(SerializedNodeReferences $nodeReferences, ReferenceName $referenceName, NodeTypeName $nodeTypeName): void
{
$nodeType = $this->requireNodeType($nodeTypeName);
$referenceDefinition = $nodeType->referenceDefinitions->get($referenceName);
if ($referenceDefinition === null) {
throw ReferenceCannotBeSet::becauseTheNodeTypeDoesNotDeclareIt(
$referenceName,
$nodeTypeName,
);
}

$maxItems = $nodeType->getReferences()[$referenceName->value]['constraints']['maxItems'] ?? null;
$maxItems = $referenceDefinition->maxItems ?? null;
if ($maxItems === null) {
return;
}
Expand Down
Expand Up @@ -59,8 +59,6 @@ abstract protected function areAncestorNodeTypeConstraintChecksEnabled(): bool;

abstract protected function requireNodeType(NodeTypeName $nodeTypeName): NodeType;

abstract protected function requireNodeTypeToNotBeAbstract(NodeType $nodeType): void;

abstract protected function requireNodeTypeToBeOfTypeRoot(NodeType $nodeType): void;

abstract protected function requireNodeTypeNotToDeclareTetheredChildNodeName(NodeTypeName $nodeTypeName, NodeName $nodeName): void;
Expand Down Expand Up @@ -133,7 +131,6 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties(
$expectedVersion = $this->getExpectedVersionOfContentStream($contentGraph->getContentStreamId(), $commandHandlingDependencies);
$this->requireDimensionSpacePointToExist($command->originDimensionSpacePoint->toDimensionSpacePoint());
$nodeType = $this->requireNodeType($command->nodeTypeName);
$this->requireNodeTypeToNotBeAbstract($nodeType);
$this->requireNodeTypeToNotBeOfTypeRoot($nodeType);
$this->requireTetheredDescendantNodeTypesToExist($nodeType);
$this->requireTetheredDescendantNodeTypesToNotBeOfTypeRoot($nodeType);
Expand Down
Expand Up @@ -60,6 +60,7 @@ public function resolveAffectedOrigins(
};
}

// TODO remove
public static function tryFromDeclaration(NodeType $nodeType, PropertyName $propertyName): self
{
$declaration = $nodeType->getProperties()[$propertyName->value]['scope'] ?? null;
Expand Down
Expand Up @@ -71,29 +71,28 @@ public static function fromArray(array $propertyValues): self
public static function defaultFromNodeType(NodeType $nodeType, PropertyConverter $propertyConverter): self
{
$values = [];
foreach ($nodeType->getDefaultValuesForProperties() as $propertyName => $defaultValue) {
foreach ($nodeType->propertyDefinitions as $propertyDefinition) {
if ($propertyDefinition->defaultValue === null) {
continue;
}
$propertyType = PropertyType::fromNodeTypeDeclaration(
$nodeType->getPropertyType($propertyName),
PropertyName::fromString($propertyName),
$nodeType->getPropertyType($propertyDefinition->name->value),
PropertyName::fromString($propertyDefinition->name->value),
$nodeType->name
);
$deserializedDefaultValue = $propertyConverter->deserializePropertyValue(
SerializedPropertyValue::create($defaultValue, $propertyType->getSerializationType())
SerializedPropertyValue::create($propertyDefinition->defaultValue, $propertyType->getSerializationType())
);
// The $defaultValue and $properlySerializedDefaultValue will likely equal, but in some cases diverge.
// For example relative date time default values like "now" will herby be serialized to the current date.
// Also, custom value objects might serialize slightly different, but more "correct"
// (by for example adding default values for undeclared properties)
// Additionally due the double conversion, we guarantee that a valid property converted exists at this time.
$properlySerializedDefaultValue = $propertyConverter->serializePropertyValue(
PropertyType::fromNodeTypeDeclaration(
$nodeType->getPropertyType($propertyName),
PropertyName::fromString($propertyName),
$nodeType->name
),
$propertyType,
$deserializedDefaultValue
);
$values[$propertyName] = $properlySerializedDefaultValue;
$values[$propertyDefinition->name->value] = $properlySerializedDefaultValue;
}

return new self($values);
Expand Down
Expand Up @@ -53,8 +53,6 @@ abstract protected function getAllowedDimensionSubspace(): DimensionSpacePointSe

abstract protected function requireNodeType(NodeTypeName $nodeTypeName): NodeType;

abstract protected function requireNodeTypeToNotBeAbstract(NodeType $nodeType): void;

abstract protected function requireNodeTypeToBeOfTypeRoot(NodeType $nodeType): void;

/**
Expand All @@ -79,7 +77,6 @@ private function handleCreateRootNodeAggregateWithNode(
$command->nodeAggregateId
);
$nodeType = $this->requireNodeType($command->nodeTypeName);
$this->requireNodeTypeToNotBeAbstract($nodeType);
$this->requireNodeTypeToBeOfTypeRoot($nodeType);
$this->requireRootNodeTypeToBeUnoccupied(
$contentGraph,
Expand Down
@@ -0,0 +1,154 @@
<?php

/*
* This file is part of the Neos.ContentRepository package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

declare(strict_types=1);

namespace Neos\ContentRepository\Core\NodeType;

use Neos\ContentRepository\Core\SharedModel\Exception\NodeConfigurationException;
use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeIsFinalException;

/**
* @internal
*/
final class ClosureNodeTypeProvider implements NodeTypeProviderInterface

Check failure on line 23 in Neos.ContentRepository.Core/Classes/NodeType/ClosureNodeTypeProvider.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test linting-unit-functionaltests-mysql (deps: highest)

Non-abstract class Neos\ContentRepository\Core\NodeType\ClosureNodeTypeProvider contains abstract method getNodeType() from interface Neos\ContentRepository\Core\NodeType\NodeTypeProviderInterface.

Check failure on line 23 in Neos.ContentRepository.Core/Classes/NodeType/ClosureNodeTypeProvider.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test linting-unit-functionaltests-mysql (deps: highest)

Non-abstract class Neos\ContentRepository\Core\NodeType\ClosureNodeTypeProvider contains abstract method getSubNodeTypeNames() from interface Neos\ContentRepository\Core\NodeType\NodeTypeProviderInterface.

Check failure on line 23 in Neos.ContentRepository.Core/Classes/NodeType/ClosureNodeTypeProvider.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 Test linting-unit-functionaltests-mysql (deps: highest)

Non-abstract class Neos\ContentRepository\Core\NodeType\ClosureNodeTypeProvider contains abstract method getNodeType() from interface Neos\ContentRepository\Core\NodeType\NodeTypeProviderInterface.

Check failure on line 23 in Neos.ContentRepository.Core/Classes/NodeType/ClosureNodeTypeProvider.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 Test linting-unit-functionaltests-mysql (deps: highest)

Non-abstract class Neos\ContentRepository\Core\NodeType\ClosureNodeTypeProvider contains abstract method getSubNodeTypeNames() from interface Neos\ContentRepository\Core\NodeType\NodeTypeProviderInterface.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would suggest LazyNodeTypeProvider::fromClosure

{
private NodeTypes $cachedNodeTypes;

public function __construct(
private readonly \Closure $nodeTypeConfigLoader,
) {
$this->cachedNodeTypes = NodeTypes::fromArray([]);
}

public function getNodeTypes(): NodeTypes
{
if ($this->cachedNodeTypes->isEmpty()) {
$completeNodeTypeConfiguration = ($this->nodeTypeConfigLoader)();
// the root node type must always exist
$completeNodeTypeConfiguration[NodeTypeName::ROOT_NODE_TYPE_NAME] ??= [];
foreach (array_keys($completeNodeTypeConfiguration) as $nodeTypeName) {
if (!is_string($nodeTypeName)) {
continue;
}
if (!is_array($completeNodeTypeConfiguration[$nodeTypeName])) {
continue;
}
$this->loadNodeType($nodeTypeName, $completeNodeTypeConfiguration);
}
}
return $this->cachedNodeTypes;
}

/**
* Load one node type, if it is not loaded yet.
*
* @param array<string,mixed> $completeNodeTypeConfiguration the full node type configuration for all node types
* @throws NodeConfigurationException
* @throws NodeTypeIsFinalException
*/
private function loadNodeType(string $nodeTypeName, array &$completeNodeTypeConfiguration): NodeType
{
$cachedNodeType = $this->cachedNodeTypes->get($nodeTypeName);
if ($cachedNodeType !== null) {
return $cachedNodeType;
}

if (!isset($completeNodeTypeConfiguration[$nodeTypeName])) {
// only thrown if a programming error occurred.
throw new \RuntimeException('Must not happen, logic error: Node type "' . $nodeTypeName . '" does not exist', 1316451800);
}

$nodeTypeConfiguration = $completeNodeTypeConfiguration[$nodeTypeName];
try {
$superTypes = isset($nodeTypeConfiguration['superTypes'])
? $this->evaluateSuperTypesConfiguration(
$nodeTypeConfiguration['superTypes'],
$completeNodeTypeConfiguration
)
: [];
} catch (NodeConfigurationException $exception) {
throw new NodeConfigurationException('Node type "' . $nodeTypeName . '" sets super type with a non-string key to NULL.', 1416578395, $exception);
} catch (NodeTypeIsFinalException $exception) {
throw new NodeTypeIsFinalException('Node type "' . $nodeTypeName . '" has a super type "' . $exception->getMessage() . '" which is final.', 1316452423, $exception);
}

// Remove unset properties
$nodeTypeConfiguration['properties'] = array_filter($nodeTypeConfiguration['properties'] ?? [], static fn ($propertyConfiguration) => $propertyConfiguration !== null);
if ($nodeTypeConfiguration['properties'] === []) {
unset($nodeTypeConfiguration['properties']);
}

$nodeType = new NodeType(

Check failure on line 91 in Neos.ContentRepository.Core/Classes/NodeType/ClosureNodeTypeProvider.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test linting-unit-functionaltests-mysql (deps: highest)

Cannot instantiate class Neos\ContentRepository\Core\NodeType\NodeType via private constructor Neos\ContentRepository\Core\NodeType\NodeType::__construct().

Check failure on line 91 in Neos.ContentRepository.Core/Classes/NodeType/ClosureNodeTypeProvider.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test linting-unit-functionaltests-mysql (deps: highest)

Class Neos\ContentRepository\Core\NodeType\NodeType constructor invoked with 3 parameters, 9 required.

Check failure on line 91 in Neos.ContentRepository.Core/Classes/NodeType/ClosureNodeTypeProvider.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 Test linting-unit-functionaltests-mysql (deps: highest)

Cannot instantiate class Neos\ContentRepository\Core\NodeType\NodeType via private constructor Neos\ContentRepository\Core\NodeType\NodeType::__construct().

Check failure on line 91 in Neos.ContentRepository.Core/Classes/NodeType/ClosureNodeTypeProvider.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 Test linting-unit-functionaltests-mysql (deps: highest)

Class Neos\ContentRepository\Core\NodeType\NodeType constructor invoked with 3 parameters, 9 required.
NodeTypeName::fromString($nodeTypeName),
$superTypes,

Check failure on line 93 in Neos.ContentRepository.Core/Classes/NodeType/ClosureNodeTypeProvider.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test linting-unit-functionaltests-mysql (deps: highest)

Parameter #2 $superTypeNames of class Neos\ContentRepository\Core\NodeType\NodeType constructor expects Neos\ContentRepository\Core\NodeType\NodeTypeNames, array<string, Neos\ContentRepository\Core\NodeType\NodeType|null> given.

Check failure on line 93 in Neos.ContentRepository.Core/Classes/NodeType/ClosureNodeTypeProvider.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 Test linting-unit-functionaltests-mysql (deps: highest)

Parameter #2 $superTypeNames of class Neos\ContentRepository\Core\NodeType\NodeType constructor expects Neos\ContentRepository\Core\NodeType\NodeTypeNames, array<string, Neos\ContentRepository\Core\NodeType\NodeType|null> given.
$nodeTypeConfiguration
);

$this->cachedNodeTypes = $this->cachedNodeTypes->with($nodeType);
return $nodeType;
}

/**
* Evaluates the given superTypes configuation and returns the array of effective superTypes.
*
* @param array<string,mixed> $superTypesConfiguration
* @param array<string,mixed> $completeNodeTypeConfiguration
* @return array<string,NodeType|null>
*/
private function evaluateSuperTypesConfiguration(
array $superTypesConfiguration,
array $completeNodeTypeConfiguration
): array {
$superTypes = [];
foreach ($superTypesConfiguration as $superTypeName => $enabled) {
if (!is_string($superTypeName)) {
throw new NodeConfigurationException(
'superTypes must be a dictionary; the array format was deprecated since Neos 2.0',
1651821391
);
}
$superTypes[$superTypeName] = $this->evaluateSuperTypeConfiguration(
$superTypeName,
$enabled,
$completeNodeTypeConfiguration
);
}

return $superTypes;
}

/**
* Evaluates a single superType configuration and returns the NodeType if enabled.
*
* @param array<string,mixed> $completeNodeTypeConfiguration
* @throws NodeConfigurationException
* @throws NodeTypeIsFinalException
*/
private function evaluateSuperTypeConfiguration(
string $superTypeName,
?bool $enabled,
array &$completeNodeTypeConfiguration
): ?NodeType {
// Skip unset node types
if ($enabled === false || $enabled === null) {
return null;
}

$superType = $this->loadNodeType($superTypeName, $completeNodeTypeConfiguration);
if ($superType->isFinal() === true) {
throw new NodeTypeIsFinalException($superType->name->value, 1444944148);
}

return $superType;
}
}
Expand Up @@ -5,6 +5,7 @@
/**
* Performs node type constraint checks against a given set of constraints
* @internal
* TODO replace with {@see NodeTypeConstraints}
*/
final readonly class ConstraintCheck
{
Expand Down Expand Up @@ -142,7 +143,7 @@
}

$distance++;
foreach ($currentNodeType->getDeclaredSuperTypes() as $superType) {

Check failure on line 146 in Neos.ContentRepository.Core/Classes/NodeType/ConstraintCheck.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test linting-unit-functionaltests-mysql (deps: highest)

Call to an undefined method Neos\ContentRepository\Core\NodeType\NodeType::getDeclaredSuperTypes().

Check failure on line 146 in Neos.ContentRepository.Core/Classes/NodeType/ConstraintCheck.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 Test linting-unit-functionaltests-mysql (deps: highest)

Call to an undefined method Neos\ContentRepository\Core\NodeType\NodeType::getDeclaredSuperTypes().
$result = $this->traverseSuperTypes($superType, $constraintNodeTypeName, $distance);
if ($result !== null) {
return $result;
Expand Down