diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cc0d7e1..383d575 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,10 +19,24 @@ jobs: fail-fast: false matrix: include: - - php-version: 7.4 - neos-version: 7.3 - - php-version: 8.1 - neos-version: 8.3 + - php-version: "8.2" + neos-version: "9.0" + - php-version: "8.3" + neos-version: "9.0" + + services: + mariadb: + # see https://mariadb.com/kb/en/mariadb-server-release-dates/ + # this should be a current release, e.g. the LTS version + image: mariadb:10.8 + env: + MYSQL_USER: neos + MYSQL_PASSWORD: neos + MYSQL_DATABASE: neos_functional_testing + MYSQL_ROOT_PASSWORD: neos + ports: + - "3306:3306" + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout code @@ -38,7 +52,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT shell: bash - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -46,7 +60,7 @@ jobs: - name: Prepare Neos distribution run: | - git clone --depth 1 --branch ${{ matrix.neos-version }} https://github.com/neos/neos-base-distribution.git ${FLOW_PATH_ROOT} + git clone --depth 1 --branch ${{ matrix.neos-version }} https://github.com/neos/neos-development-distribution.git ${FLOW_PATH_ROOT} cd ${FLOW_PATH_ROOT} composer config --no-plugins allow-plugins.neos/composer-plugin true composer config repositories.package '{ "type": "path", "url": "../Flowpack.NodeTemplates", "options": { "symlink": false } }' @@ -56,6 +70,7 @@ jobs: - name: Install dependencies run: | cd ${FLOW_PATH_ROOT} + rm -rf composer.lock composer install --no-interaction --no-progress --prefer-dist - name: Linting @@ -66,9 +81,35 @@ jobs: - name: Run Unit tests run: | cd ${FLOW_PATH_ROOT} - bin/phpunit --colors -c Build/BuildEssentials/PhpUnit/UnitTests.xml Packages/Application/Flowpack.NodeTemplates/Tests/Unit + bin/phpunit -c Build/BuildEssentials/PhpUnit/UnitTests.xml Packages/Application/Flowpack.NodeTemplates/Tests/Unit + + - name: Setup Flow configuration + run: | + cd ${FLOW_PATH_ROOT} + rm -f Configuration/Testing/Settings.yaml + cat <> Configuration/Testing/Settings.yaml + Neos: + Flow: + persistence: + backendOptions: + host: '127.0.0.1' + driver: pdo_mysql + user: 'neos' + password: 'neos' + dbname: 'neos_functional_testing' + EOF - name: Run Functional tests run: | cd ${FLOW_PATH_ROOT} - bin/phpunit --colors -c Build/BuildEssentials/PhpUnit/FunctionalTests.xml Packages/Application/Flowpack.NodeTemplates/Tests/Functional + bin/phpunit -c Build/BuildEssentials/PhpUnit/FunctionalTests.xml Packages/Application/Flowpack.NodeTemplates/Tests/Functional + + - name: Show log on failure + if: ${{ failure() }} + run: | + cd ${FLOW_PATH_ROOT} + cat Data/Logs/System_Testing.log + for file in Data/Logs/Exceptions/*; do + echo $file + cat $file + done diff --git a/Classes/Application/Command/NodeTemplateCommandController.php b/Classes/Application/Command/NodeTemplateCommandController.php index 24cc3b2..15767d5 100644 --- a/Classes/Application/Command/NodeTemplateCommandController.php +++ b/Classes/Application/Command/NodeTemplateCommandController.php @@ -8,12 +8,18 @@ use Flowpack\NodeTemplates\Domain\NodeCreation\NodeCreationService; use Flowpack\NodeTemplates\Domain\NodeTemplateDumper\NodeTemplateDumper; use Flowpack\NodeTemplates\Domain\TemplateConfiguration\TemplateConfigurationProcessor; -use Neos\ContentRepository\Domain\Model\NodeInterface; -use Neos\ContentRepository\Domain\Service\ContextFactoryInterface; -use Neos\ContentRepository\Domain\Service\NodeTypeManager; +use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; +use Neos\ContentRepository\Core\Projection\ContentGraph\AbsoluteNodePath; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; +use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; -use Neos\Neos\Domain\Service\ContentContext; +use Neos\Neos\Domain\Repository\SiteRepository; +use Neos\Neos\Domain\Service\NodeTypeNameFactory; +use Neos\Neos\Ui\Domain\NodeCreation\NodeCreationCommands; class NodeTemplateCommandController extends CommandController { @@ -23,12 +29,6 @@ class NodeTemplateCommandController extends CommandController */ protected $nodeCreationService; - /** - * @Flow\Inject - * @var ContextFactoryInterface - */ - protected $contextFactory; - /** * @Flow\Inject * @var NodeTemplateDumper @@ -42,30 +42,52 @@ class NodeTemplateCommandController extends CommandController protected $templateConfigurationProcessor; /** + * @var SiteRepository * @Flow\Inject - * @var NodeTypeManager */ - protected $nodeTypeManager; + protected $siteRepository; + + /** + * @var ContentRepositoryRegistry + * @Flow\Inject + */ + protected $contentRepositoryRegistry; /** * Dump the node tree structure into a NodeTemplate YAML structure. * References to Nodes and non-primitive property values are commented out in the YAML. * * @param string $startingNodeId specified root node of the node tree. + * @param string|null $site the Neos site, which determines the content repository. Defaults to the first available one. * @param string $workspaceName custom workspace to dump from. Defaults to 'live'. * @return void */ - public function createFromNodeSubtreeCommand(string $startingNodeId, string $workspaceName = 'live'): void + public function createFromNodeSubtreeCommand(string $startingNodeId, ?string $site = null, string $workspaceName = 'live'): void { - $subgraph = $this->contextFactory->create([ - 'workspaceName' => $workspaceName - ]); - /** @var ?NodeInterface $node */ - $node = $subgraph->getNodeByIdentifier($startingNodeId); + $siteInstance = $site + ? $this->siteRepository->findOneByNodeName($site) + : $this->siteRepository->findDefault(); + + if (!$siteInstance) { + $this->outputLine(sprintf('Site "%s" does not exist.', $site)); + $this->quit(2); + } + + $siteConfiguration = $siteInstance->getConfiguration(); + + $contentRepository = $this->contentRepositoryRegistry->get($siteConfiguration->contentRepositoryId); + + // default context? https://github.com/neos/neos-development-collection/issues/5113 + $subgraph = $contentRepository->getContentGraph(WorkspaceName::fromString($workspaceName))->getSubgraph( + $siteConfiguration->defaultDimensionSpacePoint, + VisibilityConstraints::default() + ); + + $node = $subgraph->findNodeById(NodeAggregateId::fromString($startingNodeId)); if (!$node) { throw new \InvalidArgumentException("Node $startingNodeId doesnt exist in workspace $workspaceName."); } - echo $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node); + echo $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node, $contentRepository); } /** @@ -74,9 +96,23 @@ public function createFromNodeSubtreeCommand(string $startingNodeId, string $wor * * We process and build all configured NodeType templates. No nodes will be created in the Content Repository. * + * @param string|null $site the Neos site, which determines the content repository. Defaults to the first available one. */ - public function validateCommand(): void + public function validateCommand(?string $site = null): void { + $siteInstance = $site + ? $this->siteRepository->findOneByNodeName($site) + : $this->siteRepository->findDefault(); + + if (!$siteInstance) { + $this->outputLine(sprintf('Site "%s" does not exist.', $site)); + $this->quit(2); + } + + $siteConfiguration = $siteInstance->getConfiguration(); + + $contentRepository = $this->contentRepositoryRegistry->get($siteConfiguration->contentRepositoryId); + $templatesChecked = 0; /** * nodeTypeNames as index @@ -84,15 +120,30 @@ public function validateCommand(): void */ $faultyNodeTypeTemplates = []; - foreach ($this->nodeTypeManager->getNodeTypes(false) as $nodeType) { + // default context? https://github.com/neos/neos-development-collection/issues/5113 + $subgraph = $contentRepository->getContentGraph(WorkspaceName::forLive())->getSubgraph( + $siteConfiguration->defaultDimensionSpacePoint, + VisibilityConstraints::default() + ); + + $sitesNode = $subgraph->findRootNodeByType(NodeTypeNameFactory::forSites()); + $siteNode = $sitesNode ? $subgraph->findNodeByPath( + $siteInstance->getNodeName()->toNodeName(), + $sitesNode->aggregateId + ) : null; + + if (!$siteNode) { + $this->outputLine(sprintf('Could not resolve site node for site "%s".', $siteInstance->getNodeName()->value)); + $this->quit(3); + } + + foreach ($contentRepository->getNodeTypeManager()->getNodeTypes(false) as $nodeType) { $templateConfiguration = $nodeType->getOptions()['template'] ?? null; if (!$templateConfiguration) { continue; } $processingErrors = ProcessingErrors::create(); - /** @var ContentContext $subgraph */ - $subgraph = $this->contextFactory->create(); $observableEmptyData = new class ([]) extends \ArrayObject { @@ -104,27 +155,37 @@ public function offsetExists($key): bool } }; - $siteNode = $subgraph->getCurrentSiteNode(); - $template = $this->templateConfigurationProcessor->processTemplateConfiguration( $templateConfiguration, [ 'data' => $observableEmptyData, - 'triggeringNode' => $siteNode, // @deprecated 'site' => $siteNode, 'parentNode' => $siteNode, ], $processingErrors ); - $this->nodeCreationService->createMutatorsForRootTemplate($template, $nodeType, $this->nodeTypeManager, $subgraph, $processingErrors); + $fakeNodeCreationCommands = NodeCreationCommands::fromFirstCommand( + CreateNodeAggregateWithNode::create( + $siteNode->workspaceName, + NodeAggregateId::create(), + $nodeType->name, + $siteNode->originDimensionSpacePoint, + $siteNode->aggregateId + ), + $contentRepository->getNodeTypeManager() + ); + + $this->nodeCreationService->apply($template, $fakeNodeCreationCommands, $contentRepository->getNodeTypeManager(), $subgraph, $nodeType, $processingErrors); if ($processingErrors->hasError()) { - $faultyNodeTypeTemplates[$nodeType->getName()] = ['processingErrors' => $processingErrors, 'dataWasAccessed' => $observableEmptyData->dataWasAccessed]; + $faultyNodeTypeTemplates[$nodeType->name->value] = ['processingErrors' => $processingErrors, 'dataWasAccessed' => $observableEmptyData->dataWasAccessed]; } $templatesChecked++; } + $this->output(sprintf('Content repository "%s": ', $contentRepository->id->value)); + if ($templatesChecked === 0) { $this->outputLine('No NodeType templates found.'); return; diff --git a/Classes/Domain/ErrorHandling/ErrorHandlingConfiguration.php b/Classes/Domain/ErrorHandling/ErrorHandlingConfiguration.php index 5b3fa21..168ae14 100644 --- a/Classes/Domain/ErrorHandling/ErrorHandlingConfiguration.php +++ b/Classes/Domain/ErrorHandling/ErrorHandlingConfiguration.php @@ -8,6 +8,7 @@ class ErrorHandlingConfiguration { /** * @Flow\InjectConfiguration(package="Flowpack.NodeTemplates", path="errorHandling") + * @var array */ protected array $configuration; diff --git a/Classes/Domain/ErrorHandling/ProcessingErrorHandler.php b/Classes/Domain/ErrorHandling/ProcessingErrorHandler.php index 081e090..8f412b8 100644 --- a/Classes/Domain/ErrorHandling/ProcessingErrorHandler.php +++ b/Classes/Domain/ErrorHandling/ProcessingErrorHandler.php @@ -2,11 +2,13 @@ namespace Flowpack\NodeTemplates\Domain\ErrorHandling; -use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\Flow\Annotations as Flow; use Neos\Flow\Log\ThrowableStorageInterface; use Neos\Flow\Log\Utility\LogEnvironment; use Neos\Neos\Ui\Domain\Model\Feedback\Messages\Error; +use Neos\Neos\Ui\Domain\Model\Feedback\Messages\Warning; use Neos\Neos\Ui\Domain\Model\FeedbackCollection; use Psr\Log\LoggerInterface; @@ -39,7 +41,7 @@ class ProcessingErrorHandler /** * @return bool if to continue or abort */ - public function handleAfterTemplateConfigurationProcessing(ProcessingErrors $processingErrors, NodeInterface $node): bool + public function handleAfterTemplateConfigurationProcessing(ProcessingErrors $processingErrors, NodeType $nodeType, NodeAggregateId $nodeAggregateId): bool { if (!$processingErrors->hasError()) { return true; @@ -49,9 +51,8 @@ public function handleAfterTemplateConfigurationProcessing(ProcessingErrors $pro return true; } - assert(method_exists($node, '__toString')); $templateNotCreatedException = new TemplateNotCreatedException( - sprintf('Template for "%s" was not applied. Only %s was created.', $node->getNodeType()->getLabel(), (string)$node), + sprintf('Template for "%s" was not applied. Only %s was created.', $nodeType->getLabel(), $nodeAggregateId->value), 1686135532992, $processingErrors->first()->getException(), ); @@ -64,15 +65,14 @@ public function handleAfterTemplateConfigurationProcessing(ProcessingErrors $pro /** * @return bool if to continue or abort */ - public function handleAfterNodeCreation(ProcessingErrors $processingErrors, NodeInterface $node): bool + public function handleAfterNodeCreation(ProcessingErrors $processingErrors, NodeType $nodeType, NodeAggregateId $nodeAggregateId): bool { if (!$processingErrors->hasError()) { return true; } - assert(method_exists($node, '__toString')); $templatePartiallyCreatedException = new TemplatePartiallyCreatedException( - sprintf('Template for "%s" only partially applied. Please check the newly created nodes beneath %s.', $node->getNodeType()->getLabel(), (string)$node), + sprintf('Template for "%s" only partially applied. Please check the newly created nodes beneath %s.', $nodeType->getLabel(), $nodeAggregateId->value), 1686135564160, $processingErrors->first()->getException(), ); @@ -106,10 +106,10 @@ private function logProcessingErrors(ProcessingErrors $processingErrors, \Domain ); foreach ($messages as $message) { - $error = new Error(); - $error->setMessage($message); + $warning = new Warning(); + $warning->setMessage($message); $this->feedbackCollection->add( - $error + $warning ); } } diff --git a/Classes/Domain/ErrorHandling/ProcessingErrors.php b/Classes/Domain/ErrorHandling/ProcessingErrors.php index a2e7dd7..403d525 100644 --- a/Classes/Domain/ErrorHandling/ProcessingErrors.php +++ b/Classes/Domain/ErrorHandling/ProcessingErrors.php @@ -2,9 +2,7 @@ namespace Flowpack\NodeTemplates\Domain\ErrorHandling; -use Neos\Flow\Annotations as Flow; - -/** @Flow\Proxy(false) */ +/** @implements \IteratorAggregate */ class ProcessingErrors implements \IteratorAggregate { /** @var array */ @@ -19,6 +17,7 @@ public static function create(): self return new self(); } + /** @phpstan-assert-if-true !null $this->first() */ public function hasError(): bool { return $this->errors !== []; diff --git a/Classes/Domain/NodeCreation/NodeCreationService.php b/Classes/Domain/NodeCreation/NodeCreationService.php index d6dd160..208316e 100644 --- a/Classes/Domain/NodeCreation/NodeCreationService.php +++ b/Classes/Domain/NodeCreation/NodeCreationService.php @@ -2,21 +2,40 @@ namespace Flowpack\NodeTemplates\Domain\NodeCreation; +use Behat\Transliterator\Transliterator; use Flowpack\NodeTemplates\Domain\ErrorHandling\ProcessingError; use Flowpack\NodeTemplates\Domain\ErrorHandling\ProcessingErrors; use Flowpack\NodeTemplates\Domain\Template\RootTemplate; use Flowpack\NodeTemplates\Domain\Template\Templates; -use Neos\ContentRepository\Domain\Model\NodeInterface; -use Neos\ContentRepository\Domain\Model\NodeType; -use Neos\ContentRepository\Domain\Service\Context; -use Neos\ContentRepository\Domain\Service\NodeTypeManager; +use Neos\ContentRepository\Core\CommandHandler\Commands; +use Neos\ContentRepository\Core\Dimension\ContentDimensionId; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; +use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; +use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; +use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; +use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetNodeReferences; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesForName; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferencesToWrite; +use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\NodeType\NodeTypeManager; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; +use Neos\ContentRepository\Core\SharedModel\Node\NodeName; +use Neos\ContentRepository\Core\SharedModel\Node\PropertyName; +use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\Flow\Annotations as Flow; -use Neos\Neos\Utility\NodeUriPathSegmentGenerator; +use Neos\Flow\I18n\Exception\InvalidLocaleIdentifierException; +use Neos\Flow\I18n\Locale; +use Neos\Neos\Service\TransliterationService; +use Neos\Neos\Ui\Domain\NodeCreation\NodeCreationCommands; /** * Declares the steps how to create a node subtree starting from the root template {@see RootTemplate} * - * The steps can to be applied to create the node structure via {@see NodeMutatorCollection::executeWithStartingNode()} + * The commands can then be handled by the content repository to create the node structure * * @Flow\Scope("singleton") */ @@ -24,9 +43,9 @@ class NodeCreationService { /** * @Flow\Inject - * @var NodeUriPathSegmentGenerator + * @var TransliterationService */ - protected $nodeUriPathSegmentGenerator; + protected $transliterationService; /** * @Flow\Inject @@ -41,69 +60,115 @@ class NodeCreationService protected $referencesProcessor; /** - * Creates mutator {@see NodeMutatorCollection} for the root template and its descending configured child node templates to be applied on a node. + * Creates commands {@see NodeCreationCommands} for the root template and its descending configured child node templates. * @throws \InvalidArgumentException */ - public function createMutatorsForRootTemplate(RootTemplate $template, NodeType $nodeType, NodeTypeManager $nodeTypeManager, Context $subgraph, ProcessingErrors $processingErrors): NodeMutatorCollection + public function apply(RootTemplate $template, NodeCreationCommands $commands, NodeTypeManager $nodeTypeManager, ContentSubgraphInterface $subgraph, NodeType $nodeType, ProcessingErrors $processingErrors): NodeCreationCommands { - $node = TransientNode::forRegular($nodeType, $nodeTypeManager, $subgraph, $template->getProperties()); + $node = TransientNode::forRegular( + $commands->first->nodeAggregateId, + $commands->first->workspaceName, + $commands->first->originDimensionSpacePoint, + $nodeType, + $commands->first->tetheredDescendantNodeAggregateIds, + $nodeTypeManager, + $subgraph, + $template->getProperties() + ); + + $propertyValuesToWrite = PropertyValuesToWrite::fromArray( + $this->propertiesProcessor->processAndValidateProperties($node, $processingErrors) + ); + + if (count($defaultPropertiesToUnset = iterator_to_array($propertyValuesToWrite->getPropertiesToUnset()))) { + // FIXME workaround for https://github.com/neos/neos-development-collection/issues/5154 + $setDefaultPropertiesToNull = SetNodeProperties::create( + $commands->first->workspaceName, + $commands->first->nodeAggregateId, + $commands->first->originDimensionSpacePoint, + PropertyValuesToWrite::fromArray( + array_fill_keys(array_map(fn (PropertyName $name) => $name->value, $defaultPropertiesToUnset), null) + ) + ); + $commands = $commands->withAdditionalCommands(Commands::create($setDefaultPropertiesToNull)); + } - $validProperties = array_merge( - $this->propertiesProcessor->processAndValidateProperties($node, $processingErrors), + $initialProperties = $commands->first->initialPropertyValues; + + $initialProperties = $initialProperties->merge($propertyValuesToWrite); + + $initialProperties = $this->ensureNodeHasUriPathSegment( + $nodeType, + $commands->first->nodeName, + $commands->first->originDimensionSpacePoint->toDimensionSpacePoint(), + $initialProperties + ); + + $commands = $commands->withInitialPropertyValues($initialProperties); + $setReferences = $this->createReferencesCommand( + $commands->first->workspaceName, + $commands->first->nodeAggregateId, + $commands->first->originDimensionSpacePoint, $this->referencesProcessor->processAndValidateReferences($node, $processingErrors) ); + if ($setReferences) { + $commands = $commands->withInitialReferences($setReferences->references); + } - return NodeMutatorCollection::from( - NodeMutator::setProperties($validProperties), - $this->createMutatorForUriPathSegment($template->getProperties()), - )->merge( - $this->createMutatorsForChildNodeTemplates( - $template->getChildNodes(), - $node, - $processingErrors - ) + $childNodeCommands = $this->applyTemplateRecursively( + $template->getChildNodes(), + $node, + Commands::createEmpty(), + $processingErrors ); + + return $commands->withAdditionalCommands($childNodeCommands); } - private function createMutatorsForChildNodeTemplates(Templates $templates, TransientNode $parentNode, ProcessingErrors $processingErrors): NodeMutatorCollection + private function applyTemplateRecursively(Templates $templates, TransientNode $parentNode, Commands $commands, ProcessingErrors $processingErrors): Commands { - $nodeMutators = NodeMutatorCollection::empty(); - - // `hasAutoCreatedChildNode` actually has a bug; it looks up the NodeName parameter against the raw configuration instead of the transliterated NodeName - // https://github.com/neos/neos-ui/issues/3527 - $parentNodesAutoCreatedChildNodes = $parentNode->getNodeType()->getAutoCreatedChildNodes(); foreach ($templates as $template) { - if ($template->getName() && isset($parentNodesAutoCreatedChildNodes[$template->getName()->__toString()])) { + if ($template->getName() && $parentNode->nodeType->tetheredNodeTypeDefinitions->contain($template->getName())) { /** * Case 1: Auto created child nodes */ if ($template->getType() !== null) { $processingErrors->add( - ProcessingError::fromException(new \RuntimeException(sprintf('Template cant mutate type of auto created child nodes. Got: "%s"', $template->getType()->getValue()), 1685999829307)) + ProcessingError::fromException(new \RuntimeException(sprintf('Template cant mutate type of auto created child nodes. Got: "%s"', $template->getType()->value), 1685999829307)) ); // we continue processing the node } - $node = $parentNode->forTetheredChildNode($template->getName(), $template->getProperties()); + $node = $parentNode->forTetheredChildNode( + $template->getName(), + $template->getProperties() + ); - $validProperties = array_merge( - $this->propertiesProcessor->processAndValidateProperties($node, $processingErrors), - $this->referencesProcessor->processAndValidateReferences($node, $processingErrors) + $propertiesToWrite = PropertyValuesToWrite::fromArray( + $this->propertiesProcessor->processAndValidateProperties($node, $processingErrors) ); - $nodeMutators = $nodeMutators->append( - NodeMutator::isolated( - NodeMutatorCollection::from( - NodeMutator::selectChildNode($template->getName()), - NodeMutator::setProperties($validProperties) - )->merge($this->createMutatorsForChildNodeTemplates( - $template->getChildNodes(), - $node, - $processingErrors - )) + $commands = $commands->merge(Commands::fromArray(array_filter([ + $propertiesToWrite->isEmpty() ? null : SetNodeProperties::create( + $parentNode->workspaceName, + $node->aggregateId, + $parentNode->originDimensionSpacePoint, + $propertiesToWrite + ), + $this->createReferencesCommand( + $parentNode->workspaceName, + $node->aggregateId, + $parentNode->originDimensionSpacePoint, + $this->referencesProcessor->processAndValidateReferences($node, $processingErrors) ) - ); + ]))); + $commands = $this->applyTemplateRecursively( + $template->getChildNodes(), + $node, + $commands, + $processingErrors + ); continue; } @@ -112,22 +177,22 @@ private function createMutatorsForChildNodeTemplates(Templates $templates, Trans */ if ($template->getType() === null) { $processingErrors->add( - ProcessingError::fromException(new \RuntimeException('Template requires type to be set for non auto created child nodes.', 1685999829307)) + ProcessingError::fromException(new \RuntimeException(sprintf('Template requires type to be set for non auto created child nodes.'), 1685999829307)) ); continue; } - if (!$parentNode->getNodeTypeManager()->hasNodeType($template->getType()->getValue())) { + + $nodeType = $parentNode->nodeTypeManager->getNodeType($template->getType()); + if (!$nodeType) { $processingErrors->add( - ProcessingError::fromException(new \RuntimeException(sprintf('Template requires type to be a valid NodeType. Got: "%s".', $template->getType()->getValue()), 1685999795564)) + ProcessingError::fromException(new \RuntimeException(sprintf('Template requires type to be a valid NodeType. Got: "%s".', $template->getType()->value), 1685999795564)) ); continue; } - $nodeType = $parentNode->getNodeTypeManager()->getNodeType($template->getType()->getValue()); - if ($nodeType->isAbstract()) { $processingErrors->add( - ProcessingError::fromException(new \RuntimeException(sprintf('Template requires type to be a non abstract NodeType. Got: "%s".', $template->getType()->getValue()), 1686417628976)) + ProcessingError::fromException(new \RuntimeException(sprintf('Template requires type to be a non abstract NodeType. Got: "%s".', $template->getType()->value), 1686417628976)) ); continue; } @@ -141,47 +206,125 @@ private function createMutatorsForChildNodeTemplates(Templates $templates, Trans continue; } - $node = $parentNode->forRegularChildNode($nodeType, $template->getProperties()); + $node = $parentNode->forRegularChildNode(NodeAggregateId::create(), $nodeType, $template->getProperties()); + + $nodeName = $template->getName(); - $validProperties = array_merge( - $this->propertiesProcessor->processAndValidateProperties($node, $processingErrors), - $this->referencesProcessor->processAndValidateReferences($node, $processingErrors) + $initialProperties = PropertyValuesToWrite::fromArray( + $this->propertiesProcessor->processAndValidateProperties($node, $processingErrors) ); - $nodeMutators = $nodeMutators->append( - NodeMutator::isolated( - NodeMutatorCollection::from( - NodeMutator::createAndSelectNode($template->getType(), $template->getName()), - NodeMutator::setProperties($validProperties), - $this->createMutatorForUriPathSegment($template->getProperties()) - )->merge($this->createMutatorsForChildNodeTemplates( - $template->getChildNodes(), - $node, - $processingErrors - )) - ) + $initialProperties = $this->ensureNodeHasUriPathSegment( + $nodeType, + $nodeName, + $parentNode->originDimensionSpacePoint->toDimensionSpacePoint(), + $initialProperties ); + $createNode = CreateNodeAggregateWithNode::create( + $parentNode->workspaceName, + $node->aggregateId, + $template->getType(), + $parentNode->originDimensionSpacePoint, + $parentNode->aggregateId, + initialPropertyValues: $initialProperties + )->withTetheredDescendantNodeAggregateIds($node->tetheredNodeAggregateIds); + if ($nodeName) { + $createNode = $createNode->withNodeName($nodeName); + } + + $commands = $commands->merge(Commands::fromArray(array_filter([ + $createNode, + $this->createReferencesCommand( + $parentNode->workspaceName, + $node->aggregateId, + $parentNode->originDimensionSpacePoint, + $this->referencesProcessor->processAndValidateReferences($node, $processingErrors) + ) + ]))); + + $commands = $this->applyTemplateRecursively( + $template->getChildNodes(), + $node, + $commands, + $processingErrors + ); } - return $nodeMutators; + return $commands; + } + + /** + * @param array $references + */ + private function createReferencesCommand( + WorkspaceName $workspaceName, + NodeAggregateId $nodeAggregateId, + OriginDimensionSpacePoint $originDimensionSpacePoint, + array $references + ): ?SetNodeReferences { + $referencesForName = []; + foreach ($references as $name => $nodeAggregateIds) { + $referencesForName[] = NodeReferencesForName::fromTargets( + ReferenceName::fromString($name), + $nodeAggregateIds, + ); + } + return empty($referencesForName) + ? null + : SetNodeReferences::create( + $workspaceName, + $nodeAggregateId, + $originDimensionSpacePoint, + NodeReferencesToWrite::create(...$referencesForName) + ); } /** * All document node types get a uri path segment; if it is not explicitly set in the properties, * it should be built based on the title property */ - private function createMutatorForUriPathSegment(array $properties): NodeMutator + private function ensureNodeHasUriPathSegment( + NodeType $nodeType, + ?NodeName $nodeName, + DimensionSpacePoint $dimensionSpacePoint, + PropertyValuesToWrite $propertiesToWrite + ): PropertyValuesToWrite { + if (!$nodeType->isOfType('Neos.Neos:Document')) { + return $propertiesToWrite; + } + if (isset($propertiesToWrite->values['uriPathSegment'])) { + return $propertiesToWrite; + } + + return $propertiesToWrite->withValue( + 'uriPathSegment', + $this->generateUriPathSegment( + $dimensionSpacePoint, + $propertiesToWrite->values['title'] ?? $nodeName?->value ?? uniqid('', true) + ) + ); + } + + /** + * Copied from https://github.com/neos/neos-ui/blob/6929f73ffc74b1c7b63fbf80b5c2b3152e443534/Classes/NodeCreationHandler/DocumentTitleNodeCreationHandler.php#L80 + * + * The {@see \Neos\Neos\Utility\NodeUriPathSegmentGenerator::generateUriPathSegment()} only works with whole Nodes. + * + * Duplicated code might be cleaned up via https://github.com/neos/neos-development-collection/pull/4324 + */ + private function generateUriPathSegment(DimensionSpacePoint $dimensionSpacePoint, string $text): string { - return NodeMutator::unsafeFromClosure(function (NodeInterface $nodePointer) use ($properties) { - if (!$nodePointer->getNodeType()->isOfType('Neos.Neos:Document')) { - return null; - } - if (isset($properties['uriPathSegment'])) { - return null; + $languageDimensionValue = $dimensionSpacePoint->getCoordinate(new ContentDimensionId('language')); + if ($languageDimensionValue !== null) { + try { + $language = (new Locale($languageDimensionValue))->getLanguage(); + } catch (InvalidLocaleIdentifierException $e) { + // we don't need to do anything here; we'll just transliterate the text. } - $nodePointer->setProperty('uriPathSegment', $this->nodeUriPathSegmentGenerator->generateUriPathSegment($nodePointer, $properties['title'] ?? null)); - return null; - }); + } + $transliterated = $this->transliterationService->transliterate($text, $language ?? null); + + return Transliterator::urlize($transliterated); } } diff --git a/Classes/Domain/NodeCreation/NodeMutator.php b/Classes/Domain/NodeCreation/NodeMutator.php deleted file mode 100644 index 02c5d35..0000000 --- a/Classes/Domain/NodeCreation/NodeMutator.php +++ /dev/null @@ -1,125 +0,0 @@ -mutator = $mutator; - } - - /** - * Queues to set properties on the current node. - * - * Preserves the current node pointer. - */ - public static function setProperties(array $properties): self - { - return new self(function (NodeInterface $nodePointer) use ($properties) { - foreach ($properties as $key => $value) { - $nodePointer->setProperty($key, $value); - } - return null; - }); - } - - /** - * Queues to execute the collection {@see NodeMutatorCollection} on the current node. - * Any selections made in the collection {@see self::selectChildNode()} won't change the pointer to $this current node. - * - * Preserves the current node pointer. - */ - public static function isolated(NodeMutatorCollection $nodeMutators): self - { - return new self(function (NodeInterface $nodePointer) use($nodeMutators) { - $nodeMutators->executeWithStartingNode($nodePointer); - return null; - }); - } - - /** - * Queues to select a child node of the current node. - * - * Modifies the node pointer. - */ - public static function selectChildNode(NodeName $nodeName): self - { - return new self(function (NodeInterface $nodePointer) use($nodeName) { - $nextNode = $nodePointer->getNode($nodeName->__toString()); - if (!$nextNode instanceof NodeInterface) { - assert(method_exists($nodePointer, '__toString')); - throw new \RuntimeException(sprintf('Could not select childNode %s from %s', $nodeName->__toString(), $nodePointer->__toString())); - } - return $nextNode; - }); - } - - /** - * Queues to create a new node into the current node and select it. - * - * Modifies the node pointer. - */ - public static function createAndSelectNode(NodeTypeName $nodeTypeName, ?NodeName $nodeName): self - { - return new self(function (NodeInterface $nodePointer) use($nodeTypeName, $nodeName) { - $nodeOperations = Bootstrap::$staticObjectManager->get(NodeOperations::class); // hack - return $nodeOperations->create( - $nodePointer, - [ - 'nodeType' => $nodeTypeName->getValue(), - 'nodeName' => $nodeName ? $nodeName->__toString() : null - ], - 'into' - ); - }); - } - - /** - * Queues to execute this mutator on the current node - * - * Should preserve the current node pointer! - * - * @param \Closure(NodeInterface $nodePointer): null $mutator - */ - public static function unsafeFromClosure(\Closure $mutator): self - { - return new self($mutator); - } - - /** - * Applies this operation - * For multiple operations: {@see NodeMutatorCollection::executeWithStartingNode()} - * - * @param NodeInterface $nodePointer being the current node for this operation - * @return NodeInterface a new selected/created $nodePointer or the current node in case for example only properties were set - */ - public function executeWithNodePointer(NodeInterface $nodePointer): NodeInterface - { - return ($this->mutator)($nodePointer) ?? $nodePointer; - } -} diff --git a/Classes/Domain/NodeCreation/NodeMutatorCollection.php b/Classes/Domain/NodeCreation/NodeMutatorCollection.php deleted file mode 100644 index e5c91b2..0000000 --- a/Classes/Domain/NodeCreation/NodeMutatorCollection.php +++ /dev/null @@ -1,58 +0,0 @@ -items = $items; - } - - public static function from(NodeMutator ...$items): self - { - return new self(...$items); - } - - public static function empty(): self - { - return new self(); - } - - public function append(NodeMutator ...$items): self - { - return new self(...$this->items, ...$items); - } - - public function merge(self $other): self - { - return new self(...$this->items, ...$other->items); - } - - /** - * Applies all child operations on the initial node pointer - * - * @param NodeInterface $nodePointer being the current node for the first operation - */ - public function executeWithStartingNode(NodeInterface $nodePointer): void - { - foreach ($this->items as $mutator) { - $nodePointer = $mutator->executeWithNodePointer($nodePointer); - } - } -} diff --git a/Classes/Domain/NodeCreation/PropertiesProcessor.php b/Classes/Domain/NodeCreation/PropertiesProcessor.php index 0c1f17a..69d6c9b 100644 --- a/Classes/Domain/NodeCreation/PropertiesProcessor.php +++ b/Classes/Domain/NodeCreation/PropertiesProcessor.php @@ -26,15 +26,17 @@ public function __construct(PropertyMapper $propertyMapper) * * 2. It is checked, that the property value is assignable to the property type. * In case the type is class or an array of classes, the property mapper will be used map the given type to it. If it doesn't succeed, we will log an error. + * + * @return array */ public function processAndValidateProperties(TransientNode $node, ProcessingErrors $processingErrors): array { - $nodeType = $node->getNodeType(); + $nodeType = $node->nodeType; $validProperties = []; - foreach ($node->getProperties() as $propertyName => $propertyValue) { + foreach ($node->properties as $propertyName => $propertyValue) { try { $this->assertValidPropertyName($propertyName); - if (!isset($nodeType->getProperties()[$propertyName])) { + if (!$nodeType->hasProperty($propertyName)) { throw new PropertyIgnoredException( sprintf( 'Because property is not declared in NodeType. Got value `%s`.', @@ -56,7 +58,7 @@ public function processAndValidateProperties(TransientNode $node, ProcessingErro // $messages->getFirstError() doesnt work see https://github.com/neos/flow-development-collection/issues/3370 $flattenedErrors = $messages->getFlattenedErrors(); /** @var Error $firstError */ - $firstError = current(current($flattenedErrors)); + $firstError = current(current($flattenedErrors) ?: []); throw new PropertyIgnoredException($firstError->getMessage(), 1686779371122); } } @@ -74,7 +76,7 @@ public function processAndValidateProperties(TransientNode $node, ProcessingErro $validProperties[$propertyName] = $propertyValue; } catch (PropertyIgnoredException|PropertyMappingException $exception) { $processingErrors->add( - ProcessingError::fromException($exception)->withOrigin(sprintf('Property "%s" in NodeType "%s"', $propertyName, $nodeType->getName())) + ProcessingError::fromException($exception)->withOrigin(sprintf('Property "%s" in NodeType "%s"', $propertyName, $nodeType->name->value)) ); } } @@ -85,7 +87,7 @@ public function processAndValidateProperties(TransientNode $node, ProcessingErro * In the old CR, it was common practice to set internal or meta properties via this syntax: `_hidden` but we don't allow this anymore. * @throws PropertyIgnoredException */ - private function assertValidPropertyName($propertyName): void + private function assertValidPropertyName(string|int $propertyName): void { $legacyInternalProperties = ['_accessRoles', '_contentObject', '_hidden', '_hiddenAfterDateTime', '_hiddenBeforeDateTime', '_hiddenInIndex', '_index', '_name', '_nodeType', '_removed', '_workspace']; diff --git a/Classes/Domain/NodeCreation/PropertyType.php b/Classes/Domain/NodeCreation/PropertyType.php index 2fb51cc..5f1e93d 100644 --- a/Classes/Domain/NodeCreation/PropertyType.php +++ b/Classes/Domain/NodeCreation/PropertyType.php @@ -5,7 +5,7 @@ namespace Flowpack\NodeTemplates\Domain\NodeCreation; use GuzzleHttp\Psr7\Uri; -use Neos\ContentRepository\Domain\Model\NodeType; +use Neos\ContentRepository\Core\NodeType\NodeType; use Neos\Flow\Annotations as Flow; use Psr\Http\Message\UriInterface; @@ -53,16 +53,6 @@ public static function fromPropertyOfNodeType( NodeType $nodeType ): self { $declaration = $nodeType->getPropertyType($propertyName); - if ($declaration === 'reference' || $declaration === 'references') { - throw new \DomainException( - sprintf( - 'Given property "%s" is declared as "reference" in node type "%s" and must be treated as such.', - $propertyName, - $nodeType->getName() - ), - 1685964835205 - ); - } $type = self::tryFromString($declaration); if (!$type) { throw new \DomainException( @@ -70,7 +60,7 @@ public static function fromPropertyOfNodeType( 'Given property "%s" is declared as undefined type "%s" in node type "%s"', $propertyName, $declaration, - $nodeType->getName() + $nodeType->name->value ), 1685952798732 ); @@ -173,6 +163,7 @@ public function isArray(): bool return $this->value === self::TYPE_ARRAY; } + /** @phpstan-assert-if-true !null $this->arrayOfType */ public function isArrayOf(): bool { return (bool)preg_match(self::PATTERN_ARRAY_OF, $this->value); @@ -206,7 +197,7 @@ private function getArrayOf(): string return \mb_substr($this->value, 6, -1); } - public function isMatchedBy($propertyValue): bool + public function isMatchedBy(mixed $propertyValue): bool { if (is_null($propertyValue)) { return true; diff --git a/Classes/Domain/NodeCreation/ReferenceType.php b/Classes/Domain/NodeCreation/ReferenceType.php index ce6cc98..f329102 100644 --- a/Classes/Domain/NodeCreation/ReferenceType.php +++ b/Classes/Domain/NodeCreation/ReferenceType.php @@ -4,9 +4,10 @@ namespace Flowpack\NodeTemplates\Domain\NodeCreation; -use Neos\ContentRepository\Domain\Model\NodeInterface; -use Neos\ContentRepository\Domain\Model\NodeType; -use Neos\ContentRepository\Domain\NodeAggregate\NodeAggregateIdentifier; +use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; use Neos\Flow\Annotations as Flow; /** @@ -28,24 +29,25 @@ private function __construct( } public static function fromPropertyOfNodeType( - string $propertyName, + string $referenceName, NodeType $nodeType ): self { - $declaration = $nodeType->getPropertyType($propertyName); - if ($declaration === 'reference') { - return self::reference(); + if (!$nodeType->hasReference($referenceName)) { + throw new \DomainException( + sprintf( + 'Given reference "%s" is not declared in node type "%s".', + $referenceName, + $nodeType->name->value + ), + 1685964955964 + ); } - if ($declaration === 'references') { - return self::references(); + + $maxItems = $nodeType->getReferences()[$referenceName]['constraints']['maxItems'] ?? null; + if ($maxItems === 1) { + return self::reference(); } - throw new \DomainException( - sprintf( - 'Given property "%s" is not declared as "reference" in node type "%s" and must be treated as such.', - $propertyName, - $nodeType->getName() - ), - 1685964955964 - ); + return self::references(); } public static function reference(): self @@ -73,16 +75,16 @@ public function getValue(): string return $this->value; } - public function toNodeAggregateId($referenceValue): ?NodeAggregateIdentifier + public function toNodeAggregateId(mixed $referenceValue): ?NodeAggregateId { if ($referenceValue === null) { return null; } - if ($referenceValue instanceof NodeInterface) { - return NodeAggregateIdentifier::fromString($referenceValue->getIdentifier()); + if ($referenceValue instanceof Node) { + return $referenceValue->aggregateId; } try { - return NodeAggregateIdentifier::fromString($referenceValue); + return NodeAggregateId::fromString($referenceValue); } catch (\Throwable $exception) { throw new InvalidReferenceException( sprintf( @@ -94,14 +96,10 @@ public function toNodeAggregateId($referenceValue): ?NodeAggregateIdentifier } } - /** - * @param mixed $referenceValue - * @return array - */ - public function toNodeAggregateIds($referenceValue): array + public function toNodeAggregateIds(mixed $referenceValue): NodeAggregateIds { if ($referenceValue === null) { - return []; + return NodeAggregateIds::createEmpty(); } if (is_array($referenceValue) === false) { @@ -116,12 +114,12 @@ public function toNodeAggregateIds($referenceValue): array $nodeAggregateIds = []; foreach ($referenceValue as $singleNodeAggregateOrId) { - if ($singleNodeAggregateOrId instanceof NodeInterface) { - $nodeAggregateIds[] = NodeAggregateIdentifier::fromString($singleNodeAggregateOrId->getIdentifier()); + if ($singleNodeAggregateOrId instanceof Node) { + $nodeAggregateIds[] = $singleNodeAggregateOrId->aggregateId; continue; } try { - $nodeAggregateIds[] = NodeAggregateIdentifier::fromString($singleNodeAggregateOrId); + $nodeAggregateIds[] = NodeAggregateId::fromString($singleNodeAggregateOrId); } catch (\Throwable $exception) { throw new InvalidReferenceException( sprintf( @@ -132,6 +130,6 @@ public function toNodeAggregateIds($referenceValue): array ); } } - return $nodeAggregateIds; + return NodeAggregateIds::fromArray($nodeAggregateIds); } } diff --git a/Classes/Domain/NodeCreation/ReferencesProcessor.php b/Classes/Domain/NodeCreation/ReferencesProcessor.php index eb1822a..606f125 100644 --- a/Classes/Domain/NodeCreation/ReferencesProcessor.php +++ b/Classes/Domain/NodeCreation/ReferencesProcessor.php @@ -4,15 +4,18 @@ use Flowpack\NodeTemplates\Domain\ErrorHandling\ProcessingError; use Flowpack\NodeTemplates\Domain\ErrorHandling\ProcessingErrors; -use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; class ReferencesProcessor { + /** + * @return array + */ public function processAndValidateReferences(TransientNode $node, ProcessingErrors $processingErrors): array { - $nodeType = $node->getNodeType(); + $nodeType = $node->nodeType; $validReferences = []; - foreach ($node->getReferences() as $referenceName => $referenceValue) { + foreach ($node->references as $referenceName => $referenceValue) { $referenceType = ReferenceType::fromPropertyOfNodeType($referenceName, $nodeType); try { @@ -20,42 +23,41 @@ public function processAndValidateReferences(TransientNode $node, ProcessingErro $nodeAggregateIdentifier = $referenceType->toNodeAggregateId($referenceValue); if ($nodeAggregateIdentifier === null) { // not necessary needed, but to reset in case there a default values - $validReferences[$referenceName] = null; + $validReferences[$referenceName] = NodeAggregateIds::createEmpty(); continue; } - if (!($resolvedNode = $node->getSubgraph()->getNodeByIdentifier($nodeAggregateIdentifier->__toString())) instanceof NodeInterface) { + if (!$node->subgraph->findNodeById($nodeAggregateIdentifier)) { throw new InvalidReferenceException(sprintf( 'Node with identifier "%s" does not exist.', - $nodeAggregateIdentifier->__toString() + $nodeAggregateIdentifier->value ), 1687632330292); } - $validReferences[$referenceName] = $resolvedNode; + $validReferences[$referenceName] = NodeAggregateIds::create($nodeAggregateIdentifier); continue; } if ($referenceType->isReferences()) { $nodeAggregateIdentifiers = $referenceType->toNodeAggregateIds($referenceValue); - if (count($nodeAggregateIdentifiers) === 0) { + if (count(iterator_to_array($nodeAggregateIdentifiers)) === 0) { // not necessary needed, but to reset in case there a default values - $validReferences[$referenceName] = null; + $validReferences[$referenceName] = NodeAggregateIds::createEmpty(); continue; } - $nodes = []; foreach ($nodeAggregateIdentifiers as $nodeAggregateIdentifier) { - if (!($nodes[] = $node->getSubgraph()->getNodeByIdentifier($nodeAggregateIdentifier->__toString())) instanceof NodeInterface) { + if (!$node->subgraph->findNodeById($nodeAggregateIdentifier)) { throw new InvalidReferenceException(sprintf( 'Node with identifier "%s" does not exist.', - $nodeAggregateIdentifier->__toString() + $nodeAggregateIdentifier->value ), 1687632330292); } } - $validReferences[$referenceName] = $nodes; + $validReferences[$referenceName] = $nodeAggregateIdentifiers; continue; } } catch (InvalidReferenceException $runtimeException) { $processingErrors->add( ProcessingError::fromException($runtimeException) - ->withOrigin(sprintf('Reference "%s" in NodeType "%s"', $referenceName, $nodeType->getName())) + ->withOrigin(sprintf('Reference "%s" in NodeType "%s"', $referenceName, $nodeType->name->value)) ); continue; } diff --git a/Classes/Domain/NodeCreation/TransientNode.php b/Classes/Domain/NodeCreation/TransientNode.php index b591894..f0780c5 100644 --- a/Classes/Domain/NodeCreation/TransientNode.php +++ b/Classes/Domain/NodeCreation/TransientNode.php @@ -2,10 +2,16 @@ namespace Flowpack\NodeTemplates\Domain\NodeCreation; -use Neos\ContentRepository\Domain\Model\NodeType; -use Neos\ContentRepository\Domain\NodeAggregate\NodeName; -use Neos\ContentRepository\Domain\Service\Context; -use Neos\ContentRepository\Domain\Service\NodeTypeManager; +use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; +use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths; +use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\NodeType\NodeTypeManager; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeName; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\Flow\Annotations as Flow; /** @@ -23,70 +29,130 @@ * * @Flow\Proxy(false) */ -class TransientNode +final readonly class TransientNode { - private NodeType $nodeType; - - private ?NodeName $tetheredNodeName; - - private ?NodeType $tetheredParentNodeType; - - private NodeTypeManager $nodeTypeManager; - - private Context $subgraph; - - private array $properties; - - private array $references; - - private function __construct(NodeType $nodeType, ?NodeName $tetheredNodeName, ?NodeType $tetheredParentNodeType, NodeTypeManager $nodeTypeManager, Context $subgraph, array $rawProperties) - { - $this->nodeType = $nodeType; - $this->tetheredNodeName = $tetheredNodeName; - $this->tetheredParentNodeType = $tetheredParentNodeType; - if ($tetheredNodeName !== null) { - assert($tetheredParentNodeType !== null); + /** @var array */ + public array $properties; + + /** @var array */ + public array $references; + + /** @param array $rawProperties */ + private function __construct( + public NodeAggregateId $aggregateId, + public WorkspaceName $workspaceName, + public OriginDimensionSpacePoint $originDimensionSpacePoint, + public NodeType $nodeType, + public NodeAggregateIdsByNodePaths $tetheredNodeAggregateIds, + private ?NodeName $tetheredNodeName, + private ?NodeType $tetheredParentNodeType, + public NodeTypeManager $nodeTypeManager, + public ContentSubgraphInterface $subgraph, + array $rawProperties + ) { + if ($this->tetheredNodeName !== null) { + assert($this->tetheredParentNodeType !== null); } - $this->nodeTypeManager = $nodeTypeManager; - $this->subgraph = $subgraph; // split properties and references by type declaration $properties = []; $references = []; foreach ($rawProperties as $propertyName => $propertyValue) { - // TODO: remove the next line to initialise the nodeType, once https://github.com/neos/neos-development-collection/issues/4333 is fixed - $this->nodeType->getFullConfiguration(); - $declaration = $this->nodeType->getPropertyType($propertyName); - if ($declaration === 'reference' || $declaration === 'references') { + if ($this->nodeType->hasReference($propertyName)) { $references[$propertyName] = $propertyValue; continue; } + // invalid properties (!hasProperty) will be filtered out in the PropertiesProcessor $properties[$propertyName] = $propertyValue; } $this->properties = $properties; $this->references = $references; } - public static function forRegular(NodeType $nodeType, NodeTypeManager $nodeTypeManager, Context $subgraph, array $rawProperties): self - { - return new self($nodeType, null, null, $nodeTypeManager, $subgraph, $rawProperties); + /** @param array $rawProperties */ + public static function forRegular( + NodeAggregateId $nodeAggregateId, + WorkspaceName $workspaceName, + OriginDimensionSpacePoint $originDimensionSpacePoint, + NodeType $nodeType, + NodeAggregateIdsByNodePaths $tetheredNodeAggregateIds, + NodeTypeManager $nodeTypeManager, + ContentSubgraphInterface $subgraph, + array $rawProperties + ): self { + return new self( + $nodeAggregateId, + $workspaceName, + $originDimensionSpacePoint, + $nodeType, + $tetheredNodeAggregateIds, + null, + null, + $nodeTypeManager, + $subgraph, + $rawProperties + ); } + /** @param array $rawProperties */ public function forTetheredChildNode(NodeName $nodeName, array $rawProperties): self { - // `getTypeOfAutoCreatedChildNode` actually has a bug; it looks up the NodeName parameter against the raw configuration instead of the transliterated NodeName - // https://github.com/neos/neos-ui/issues/3527 - $parentNodesAutoCreatedChildNodes = $this->nodeType->getAutoCreatedChildNodes(); - $childNodeType = $parentNodesAutoCreatedChildNodes[$nodeName->__toString()] ?? null; - if (!$childNodeType instanceof NodeType) { + $nodeAggregateId = $this->tetheredNodeAggregateIds->getNodeAggregateId(NodePath::fromNodeNames($nodeName)); + + $tetheredNodeTypeDefinition = $this->nodeType->tetheredNodeTypeDefinitions->get($nodeName); + + if (!$nodeAggregateId || !$tetheredNodeTypeDefinition) { throw new \InvalidArgumentException('forTetheredChildNode only works for tethered nodes.'); } - return new self($childNodeType, $nodeName, $this->nodeType, $this->nodeTypeManager, $this->subgraph, $rawProperties); + + $childNodeType = $this->nodeTypeManager->getNodeType($tetheredNodeTypeDefinition->nodeTypeName); + if (!$childNodeType) { + throw new \InvalidArgumentException(sprintf('NodeType "%s" for tethered node "%s" does not exist.', $tetheredNodeTypeDefinition->nodeTypeName->value, $nodeName->value), 1718950833); + } + + $descendantTetheredNodeAggregateIds = NodeAggregateIdsByNodePaths::createEmpty(); + foreach ($this->tetheredNodeAggregateIds->getNodeAggregateIds() as $stringNodePath => $descendantNodeAggregateId) { + $nodePath = NodePath::fromString($stringNodePath); + $pathParts = $nodePath->getParts(); + $firstPart = array_shift($pathParts); + if ($firstPart?->equals($nodeName) && count($pathParts)) { + $descendantTetheredNodeAggregateIds = $descendantTetheredNodeAggregateIds->add( + NodePath::fromNodeNames(...$pathParts), + $descendantNodeAggregateId + ); + } + } + + return new self( + $nodeAggregateId, + $this->workspaceName, + $this->originDimensionSpacePoint, + $childNodeType, + $descendantTetheredNodeAggregateIds, + $nodeName, + $this->nodeType, + $this->nodeTypeManager, + $this->subgraph, + $rawProperties + ); } - public function forRegularChildNode(NodeType $nodeType, array $rawProperties): self + /** @param array $rawProperties */ + public function forRegularChildNode(NodeAggregateId $nodeAggregateId, NodeType $nodeType, array $rawProperties): self { - return new self($nodeType, null, null, $this->nodeTypeManager, $this->subgraph, $rawProperties); + $tetheredNodeAggregateIds = NodeAggregateIdsByNodePaths::createForNodeType($nodeType->name, $this->nodeTypeManager); + return new self( + $nodeAggregateId, + $this->workspaceName, + $this->originDimensionSpacePoint, + $nodeType, + $tetheredNodeAggregateIds, + null, + null, + $this->nodeTypeManager, + $this->subgraph, + $rawProperties + ); } /** @@ -94,64 +160,48 @@ public function forRegularChildNode(NodeType $nodeType, array $rawProperties): s */ public function requireConstraintsImposedByAncestorsToBeMet(NodeType $childNodeType): void { - if ($this->tetheredNodeName) { - self::requireNodeTypeConstraintsImposedByGrandparentToBeMet($this->tetheredParentNodeType, $this->tetheredNodeName, $childNodeType); + if ($this->isTethered()) { + $this->requireNodeTypeConstraintsImposedByGrandparentToBeMet($this->tetheredParentNodeType->name, $this->tetheredNodeName, $childNodeType->name); } else { self::requireNodeTypeConstraintsImposedByParentToBeMet($this->nodeType, $childNodeType); } } - public function getNodeType(): NodeType - { - return $this->nodeType; - } - - public function getNodeTypeManager(): NodeTypeManager - { - return $this->nodeTypeManager; - } - - public function getSubgraph(): Context - { - return $this->subgraph; - } - - public function getProperties(): array - { - return $this->properties; - } - - public function getReferences(): array - { - return $this->references; - } - private static function requireNodeTypeConstraintsImposedByParentToBeMet(NodeType $parentNodeType, NodeType $nodeType): void { if (!$parentNodeType->allowsChildNodeType($nodeType)) { throw new NodeConstraintException( sprintf( 'Node type "%s" is not allowed for child nodes of type %s', - $nodeType->getName(), - $parentNodeType->getName() + $nodeType->name->value, + $parentNodeType->name->value ), 1686417627173 ); } } - private static function requireNodeTypeConstraintsImposedByGrandparentToBeMet(NodeType $grandParentNodeType, NodeName $nodeName, NodeType $nodeType): void + private function requireNodeTypeConstraintsImposedByGrandparentToBeMet(NodeTypeName $parentNodeTypeName, NodeName $tetheredNodeName, NodeTypeName $nodeTypeNameToCheck): void { - if (!$grandParentNodeType->allowsGrandchildNodeType($nodeName->__toString(), $nodeType)) { + if (!$this->nodeTypeManager->isNodeTypeAllowedAsChildToTetheredNode($parentNodeTypeName, $tetheredNodeName, $nodeTypeNameToCheck)) { throw new NodeConstraintException( sprintf( 'Node type "%s" is not allowed below tethered child nodes "%s" of nodes of type "%s"', - $nodeType->getName(), - $nodeName->__toString(), - $grandParentNodeType->getName() + $nodeTypeNameToCheck->value, + $tetheredNodeName->value, + $parentNodeTypeName->value ), 1687541480146 ); } } + + /** + * @phpstan-assert-if-true !null $this->tetheredNodeName + * @phpstan-assert-if-true !null $this->tetheredParentNodeType + */ + private function isTethered(): bool + { + return $this->tetheredNodeName !== null; + } } diff --git a/Classes/Domain/NodeTemplateDumper/Comments.php b/Classes/Domain/NodeTemplateDumper/Comments.php index bf18141..5226090 100644 --- a/Classes/Domain/NodeTemplateDumper/Comments.php +++ b/Classes/Domain/NodeTemplateDumper/Comments.php @@ -53,6 +53,6 @@ public function renderCommentsInYamlDump(string $yamlDump): string throw new \Exception('Error while trying to render comment ' . $matches[0] . '. Reason: comment id doesnt exist.', 1684309524383); } return $comment->toYamlComment($indentation, $property); - }, $yamlDump); + }, $yamlDump) ?? throw new \Exception('Error in preg_replace_callback while trying to render comments.'); } } diff --git a/Classes/Domain/NodeTemplateDumper/NodeTemplateDumper.php b/Classes/Domain/NodeTemplateDumper/NodeTemplateDumper.php index 7be4f6c..ee7d242 100644 --- a/Classes/Domain/NodeTemplateDumper/NodeTemplateDumper.php +++ b/Classes/Domain/NodeTemplateDumper/NodeTemplateDumper.php @@ -4,11 +4,16 @@ namespace Flowpack\NodeTemplates\Domain\NodeTemplateDumper; -use Neos\ContentRepository\Domain\Model\ArrayPropertyCollection; -use Neos\ContentRepository\Domain\Model\NodeInterface; -use Neos\ContentRepository\Domain\Projection\Content\PropertyCollectionInterface; +use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindReferencesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; use Neos\Flow\Annotations as Flow; use Neos\Flow\I18n\EelHelper\TranslationHelper; +use Neos\Neos\Domain\NodeLabel\NodeLabelGeneratorInterface; use Symfony\Component\Yaml\Yaml; /** @Flow\Scope("singleton") */ @@ -20,28 +25,36 @@ class NodeTemplateDumper */ protected $translationHelper; + /** + * @var NodeLabelGeneratorInterface + * @Flow\Inject + */ + protected $nodeLabelGenerator; + /** * Dump the node tree structure into a NodeTemplate YAML structure. * References to Nodes and non-primitive property values are commented out in the YAML. * - * @param NodeInterface $startingNode specified root node of the node tree to dump + * @param Node $startingNode specified root node of the node tree to dump * @return string YAML representation of the node template */ - public function createNodeTemplateYamlDumpFromSubtree(NodeInterface $startingNode): string + public function createNodeTemplateYamlDumpFromSubtree(Node $startingNode, ContentRepository $contentRepository): string { $comments = Comments::empty(); - $nodeType = $startingNode->getNodeType(); + $nodeType = $contentRepository->getNodeTypeManager()->getNodeType($startingNode->nodeTypeName); if ( - !$nodeType->isOfType('Neos.Neos:Document') - && !$nodeType->isOfType('Neos.Neos:Content') - && !$nodeType->isOfType('Neos.Neos:ContentCollection') + !$nodeType || ( + !$nodeType->isOfType('Neos.Neos:Document') + && !$nodeType->isOfType('Neos.Neos:Content') + && !$nodeType->isOfType('Neos.Neos:ContentCollection') + ) ) { - throw new \InvalidArgumentException("Node {$startingNode->getIdentifier()} must be one of Neos.Neos:Document,Neos.Neos:Content,Neos.Neos:ContentCollection."); + throw new \InvalidArgumentException("Node {$startingNode->aggregateId->value} must be one of Neos.Neos:Document,Neos.Neos:Content,Neos.Neos:ContentCollection."); } - $template = $this->nodeTemplateFromNodes([$startingNode], $comments); + $template = $this->nodeTemplateFromNodes(Nodes::fromArray([$startingNode]), $comments, $contentRepository); $firstEntry = null; foreach ($template as $firstEntry) { @@ -53,7 +66,7 @@ public function createNodeTemplateYamlDumpFromSubtree(NodeInterface $startingNod $templateInNodeTypeOptions = [ - $nodeType->getName() => [ + $nodeType->name->value => [ 'options' => [ 'template' => array_filter([ 'properties' => $properties, @@ -68,21 +81,32 @@ public function createNodeTemplateYamlDumpFromSubtree(NodeInterface $startingNod return $comments->renderCommentsInYamlDump($yamlWithSerializedComments); } - /** @param array $nodes */ - private function nodeTemplateFromNodes(array $nodes, Comments $comments): array + /** @return array> */ + private function nodeTemplateFromNodes(Nodes $nodes, Comments $comments, ContentRepository $contentRepository): array { + $subgraph = null; + $documentNodeTemplates = []; $contentNodeTemplates = []; foreach ($nodes as $index => $node) { - assert($node instanceof NodeInterface); - $nodeType = $node->getNodeType(); + $subgraph ??= $contentRepository->getContentGraph($node->workspaceName)->getSubgraph( + $node->dimensionSpacePoint, + $node->visibilityConstraints + ); + + $nodeType = $contentRepository->getNodeTypeManager()->getNodeType($node->nodeTypeName); + if (!$nodeType) { + throw new \RuntimeException("NodeType {$node->nodeTypeName->value} of Node {$node->aggregateId->value} doesnt exist."); + } + $isDocumentNode = $nodeType->isOfType('Neos.Neos:Document'); $templatePart = array_filter([ - 'properties' => $this->nonDefaultConfiguredNodeProperties($node, $comments), + 'properties' => $this->nonDefaultConfiguredNodeProperties($node, $nodeType, $comments, $subgraph), 'childNodes' => $this->nodeTemplateFromNodes( - $node->getChildNodes('Neos.Neos:Node'), - $comments + $subgraph->findChildNodes($node->aggregateId, FindChildNodesFilter::create('Neos.Neos:Node')), + $comments, + $contentRepository ) ]); @@ -91,38 +115,44 @@ private function nodeTemplateFromNodes(array $nodes, Comments $comments): array } if ($isDocumentNode) { - if ($node->isAutoCreated()) { - $documentNodeTemplates[$node->getLabel() ?: $node->getName()] = array_merge([ - 'name' => $node->getName() + if ($node->classification->isTethered()) { + $tetheredName = $node->name; + assert($tetheredName !== null); + + $documentNodeTemplates[$this->nodeLabelGenerator->getLabel($node) ?: $tetheredName->value] = array_merge([ + 'name' => $tetheredName->value ], $templatePart); continue; } $documentNodeTemplates["page$index"] = array_merge([ - 'type' => $node->getNodeType()->getName() + 'type' => $node->nodeTypeName->value ], $templatePart); continue; } - if ($node->isAutoCreated()) { - $contentNodeTemplates[$node->getLabel() ?: $node->getName()] = array_merge([ - 'name' => $node->getName() + if ($node->classification->isTethered()) { + $tetheredName = $node->name; + assert($tetheredName !== null); + + $contentNodeTemplates[$this->nodeLabelGenerator->getLabel($node) ?: $tetheredName->value] = array_merge([ + 'name' => $tetheredName->value ], $templatePart); continue; } $contentNodeTemplates["content$index"] = array_merge([ - 'type' => $node->getNodeType()->getName() + 'type' => $node->nodeTypeName->value ], $templatePart); } return array_merge($contentNodeTemplates, $documentNodeTemplates); } - private function nonDefaultConfiguredNodeProperties(NodeInterface $node, Comments $comments): array + /** @return array */ + private function nonDefaultConfiguredNodeProperties(Node $node, NodeType $nodeType, Comments $comments, ContentSubgraphInterface $subgraph): array { - $nodeType = $node->getNodeType(); - $nodeProperties = $node->getProperties(); + $nodeProperties = $node->properties; $filteredProperties = []; foreach ($nodeType->getProperties() as $propertyName => $configuration) { @@ -170,22 +200,12 @@ function ($indentation, $propertyName) use ($dataSourceIdentifier, $propertyValu continue; } - if (($configuration['type'] ?? null) === 'reference') { - $nodeTypesInReference = $configuration['ui']['inspector']['editorOptions']['nodeTypes'] ?? ['Neos.Neos:Document']; - $filteredProperties[$propertyName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer( - function ($indentation, $propertyName) use ($nodeTypesInReference, $propertyValue) { - return $indentation . '# ' . $propertyName . ' -> Reference of NodeTypes (' . join(', ', $nodeTypesInReference) . ') with value ' . $this->valueToDebugString($propertyValue); - } - ))); - continue; - } - if (($configuration['ui']['inspector']['editor'] ?? null) === 'Neos.Neos/Inspector/Editors/SelectBoxEditor') { $selectBoxValues = array_keys($configuration['ui']['inspector']['editorOptions']['values'] ?? []); $filteredProperties[$propertyName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer( function ($indentation, $propertyName) use ($selectBoxValues, $propertyValue) { return $indentation . '# ' . $propertyName . ' -> SelectBox of ' - . mb_strimwidth(json_encode($selectBoxValues), 0, 60, ' ...]') + . mb_strimwidth(json_encode($selectBoxValues, JSON_THROW_ON_ERROR), 0, 60, ' ...]') . ' with value ' . $this->valueToDebugString($propertyValue); } ))); @@ -208,36 +228,71 @@ function ($indentation, $propertyName) use ($propertyValue) { ))); } + if ($nodeType->getReferences() === []) { + return $filteredProperties; + } + + $references = $subgraph->findReferences($node->aggregateId, FindReferencesFilter::create()); + $referencesArray = []; + foreach ($references as $reference) { + if (!isset($referencesArray[$reference->name->value])) { + $referencesArray[$reference->name->value] = $reference->node->aggregateId->value; + continue; + } + $referencesArray[$reference->name->value] .= ', ' . $reference->node->aggregateId->value; + } + + foreach ($nodeType->getReferences() as $referenceName => $configuration) { + $referenceValue = $referencesArray[$referenceName] ?? null; + if (!$referenceValue) { + continue; + } + + $label = $configuration['ui']['label'] ?? null; + $augmentCommentWithLabel = fn (Comment $comment) => $comment; + if ($label) { + $label = $this->translationHelper->translate($label); + $augmentCommentWithLabel = fn (Comment $comment) => Comment::fromRenderer( + function ($indentation, $propertyName) use($comment, $label) { + return $indentation . '# ' . $label . "\n" . + $comment->toYamlComment($indentation, $propertyName); + } + ); + } + + if (($configuration['constraints']['maxItems'] ?? null) === 1) { + $nodeTypesInReference = $configuration['ui']['inspector']['editorOptions']['nodeTypes'] ?? ['Neos.Neos:Document']; + $filteredProperties[$referenceName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer( + function ($indentation, $propertyName) use ($nodeTypesInReference, $referenceValue) { + return $indentation . '# ' . $propertyName . ' -> Reference of NodeTypes (' . join(', ', $nodeTypesInReference) . ') with Node: ' . $referenceValue; + } + ))); + continue; + } + + $filteredProperties[$referenceName] = $comments->addCommentAndGetMarker($augmentCommentWithLabel(Comment::fromRenderer( + function ($indentation, $propertyName) use ($referenceValue) { + return $indentation . '# ' . $propertyName . ' -> References with Nodes: ' . $referenceValue; + } + ))); + } + return $filteredProperties; } - private function valueToDebugString($value): string + private function valueToDebugString(mixed $value): string { - if ($value instanceof NodeInterface) { - return 'Node(' . $value->getIdentifier() . ')'; - } if (is_iterable($value)) { - $name = null; $entries = []; foreach ($value as $key => $item) { - if ($item instanceof NodeInterface) { - if ($name === null || $name === 'Nodes') { - $name = 'Nodes'; - } else { - $name = 'array'; - } - $entries[$key] = $item->getIdentifier(); - continue; - } - $name = 'array'; $entries[$key] = is_object($item) ? get_class($item) : json_encode($item); } - return $name . '(' . join(', ', $entries) . ')'; + return 'array(' . join(', ', $entries) . ')'; } if (is_object($value)) { return 'object(' . get_class($value) . ')'; } - return json_encode($value); + return json_encode($value, JSON_THROW_ON_ERROR); } } diff --git a/Classes/Domain/Template/RootTemplate.php b/Classes/Domain/Template/RootTemplate.php index 229b7cd..fca37a1 100644 --- a/Classes/Domain/Template/RootTemplate.php +++ b/Classes/Domain/Template/RootTemplate.php @@ -47,7 +47,7 @@ public function getChildNodes(): Templates return $this->childNodes; } - public function jsonSerialize(): array + public function jsonSerialize(): mixed { return [ 'properties' => $this->properties, diff --git a/Classes/Domain/Template/Template.php b/Classes/Domain/Template/Template.php index c8f98ef..645bbb6 100644 --- a/Classes/Domain/Template/Template.php +++ b/Classes/Domain/Template/Template.php @@ -3,8 +3,8 @@ namespace Flowpack\NodeTemplates\Domain\Template; -use Neos\ContentRepository\Domain\NodeAggregate\NodeName; -use Neos\ContentRepository\Domain\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\Flow\Annotations as Flow; /** @@ -60,7 +60,7 @@ public function getChildNodes(): Templates return $this->childNodes; } - public function jsonSerialize(): array + public function jsonSerialize(): mixed { return [ 'type' => $this->type, diff --git a/Classes/Domain/Template/Templates.php b/Classes/Domain/Template/Templates.php index a7f65c3..bac3351 100644 --- a/Classes/Domain/Template/Templates.php +++ b/Classes/Domain/Template/Templates.php @@ -2,12 +2,10 @@ namespace Flowpack\NodeTemplates\Domain\Template; -use Neos\Flow\Annotations as Flow; - /** * A collection of child templates {@see Template} * - * @Flow\Proxy(false) + * @implements \IteratorAggregate */ class Templates implements \IteratorAggregate, \JsonSerializable { @@ -57,7 +55,7 @@ public function toRootTemplate(): RootTemplate return RootTemplate::empty(); } - public function jsonSerialize(): array + public function jsonSerialize(): mixed { return $this->items; } diff --git a/Classes/Domain/TemplateConfiguration/EelEvaluationService.php b/Classes/Domain/TemplateConfiguration/EelEvaluationService.php index 80d99c2..98303ea 100644 --- a/Classes/Domain/TemplateConfiguration/EelEvaluationService.php +++ b/Classes/Domain/TemplateConfiguration/EelEvaluationService.php @@ -20,9 +20,11 @@ class EelEvaluationService /** * @Flow\InjectConfiguration(path="defaultEelContext") + * @var array */ protected array $defaultContextConfiguration; + /** @var array */ protected ?array $defaultContextVariables = null; /** diff --git a/Classes/Domain/TemplateConfiguration/TemplateConfigurationProcessor.php b/Classes/Domain/TemplateConfiguration/TemplateConfigurationProcessor.php index 5e5f189..406a15c 100644 --- a/Classes/Domain/TemplateConfiguration/TemplateConfigurationProcessor.php +++ b/Classes/Domain/TemplateConfiguration/TemplateConfigurationProcessor.php @@ -6,9 +6,8 @@ use Flowpack\NodeTemplates\Domain\Template\RootTemplate; use Flowpack\NodeTemplates\Domain\Template\Template; use Flowpack\NodeTemplates\Domain\Template\Templates; -use Neos\ContentRepository\Domain\NodeAggregate\NodeName; -use Neos\ContentRepository\Domain\NodeType\NodeTypeName; -use Neos\ContentRepository\Utility; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\Flow\Annotations as Flow; /** @@ -138,7 +137,7 @@ private function createTemplateFromTemplatePart(TemplatePart $templatePart): Tem $name = $templatePart->processConfiguration('name'); return new Template( $type !== null ? NodeTypeName::fromString($type) : null, - $name !== null ? NodeName::fromString(Utility::renderValidNodeName($name)) : null, + $name !== null ? NodeName::transliterateFromString($name) : null, $processedProperties, $childNodeTemplates ); diff --git a/Classes/Domain/TemplateConfiguration/TemplatePart.php b/Classes/Domain/TemplateConfiguration/TemplatePart.php index d204ca6..6b93824 100644 --- a/Classes/Domain/TemplateConfiguration/TemplatePart.php +++ b/Classes/Domain/TemplateConfiguration/TemplatePart.php @@ -4,43 +4,37 @@ use Flowpack\NodeTemplates\Domain\ErrorHandling\ProcessingError; use Flowpack\NodeTemplates\Domain\ErrorHandling\ProcessingErrors; -use Neos\Flow\Annotations as Flow; /** * @internal implementation detail of {@see TemplateConfigurationProcessor} - * @psalm-immutable - * @Flow\Proxy(false) */ -class TemplatePart +final readonly class TemplatePart { /** - * @psalm-readonly + * @var array */ private array $configuration; /** - * @psalm-readonly + * @var list */ private array $fullPathToConfiguration; /** - * @psalm-readonly + * @var array */ private array $evaluationContext; /** - * @psalm-readonly * @var \Closure(mixed $value, array $evaluationContext): mixed */ private \Closure $configurationValueProcessor; - /** - * @psalm-readonly - */ private ProcessingErrors $processingErrors; /** - * @param array $configuration + * @param array $configuration + * @param list $fullPathToConfiguration * @param array $evaluationContext * @param \Closure(mixed $value, array $evaluationContext): mixed $configurationValueProcessor * @throws StopBuildingTemplatePartException @@ -82,9 +76,9 @@ public static function createRoot( } /** - * @param string|list $configurationPath + * @param string|int|list $configurationPath */ - public function addProcessingErrorForPath(\Throwable $throwable, $configurationPath): void + public function addProcessingErrorForPath(\Throwable $throwable, array|string|int $configurationPath): void { $this->processingErrors->add( ProcessingError::fromException( @@ -96,6 +90,7 @@ public function addProcessingErrorForPath(\Throwable $throwable, $configurationP ); } + /** @return list */ public function getFullPathToConfiguration(): array { return $this->fullPathToConfiguration; @@ -138,7 +133,7 @@ public function withMergedEvaluationContext(array $evaluationContext): self * @return mixed * @throws StopBuildingTemplatePartException */ - public function processConfiguration($configurationPath) + public function processConfiguration(string|array $configurationPath): mixed { if (($value = $this->getRawConfiguration($configurationPath)) === null) { return null; @@ -166,12 +161,10 @@ public function processConfiguration($configurationPath) /** * Minimal implementation of {@see \Neos\Utility\Arrays::getValueByPath()} (but we dont allow $configurationPath to contain dots.) * - * @param string|list $configurationPath + * @psalm-param string|list $configurationPath */ - public function getRawConfiguration($configurationPath) + public function getRawConfiguration(array|string $configurationPath): mixed { - /** @phpstan-ignore-next-line */ - assert(is_array($configurationPath) || is_string($configurationPath)); $path = is_array($configurationPath) ? $configurationPath : [$configurationPath]; $array = $this->configuration; foreach ($path as $key) { @@ -187,10 +180,8 @@ public function getRawConfiguration($configurationPath) /** * @param string|list $configurationPath */ - public function hasConfiguration($configurationPath): bool + public function hasConfiguration(array|string $configurationPath): bool { - /** @phpstan-ignore-next-line */ - assert(is_array($configurationPath) || is_string($configurationPath)); $path = is_array($configurationPath) ? $configurationPath : [$configurationPath]; $array = $this->configuration; foreach ($path as $key) { diff --git a/Classes/Domain/TemplateNodeCreationHandler.php b/Classes/Domain/TemplateNodeCreationHandler.php index 74f8a02..cbe6ec8 100644 --- a/Classes/Domain/TemplateNodeCreationHandler.php +++ b/Classes/Domain/TemplateNodeCreationHandler.php @@ -6,80 +6,65 @@ use Flowpack\NodeTemplates\Domain\ErrorHandling\ProcessingErrorHandler; use Flowpack\NodeTemplates\Domain\NodeCreation\NodeCreationService; use Flowpack\NodeTemplates\Domain\TemplateConfiguration\TemplateConfigurationProcessor; -use Neos\ContentRepository\Domain\Model\NodeInterface; -use Neos\ContentRepository\Domain\Service\NodeTypeManager; -use Neos\Flow\Annotations as Flow; -use Neos\Neos\Domain\Service\ContentContext; -use Neos\Neos\Ui\NodeCreationHandler\NodeCreationHandlerInterface; - -class TemplateNodeCreationHandler implements NodeCreationHandlerInterface +use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindClosestNodeFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; +use Neos\Neos\Domain\Service\NodeTypeNameFactory; +use Neos\Neos\Ui\Domain\NodeCreation\NodeCreationCommands; +use Neos\Neos\Ui\Domain\NodeCreation\NodeCreationElements; +use Neos\Neos\Ui\Domain\NodeCreation\NodeCreationHandlerInterface; + +final readonly class TemplateNodeCreationHandler implements NodeCreationHandlerInterface { - /** - * @var NodeCreationService - * @Flow\Inject - */ - protected $nodeCreationService; - - /** - * @var NodeTypeManager - * @Flow\Inject - */ - protected $nodeTypeManager; - - /** - * @var TemplateConfigurationProcessor - * @Flow\Inject - */ - protected $templateConfigurationProcessor; - - /** - * @var ProcessingErrorHandler - * @Flow\Inject - */ - protected $processingErrorHandler; + public function __construct( + private ContentRepository $contentRepository, + private NodeCreationService $nodeCreationService, + private TemplateConfigurationProcessor $templateConfigurationProcessor, + private ProcessingErrorHandler $processingErrorHandler + ) { + } /** * Create child nodes and change properties upon node creation - * - * @param NodeInterface $node The newly created node - * @param array $data incoming data from the creationDialog */ - public function handle(NodeInterface $node, array $data): void - { - if (!$node->getNodeType()->hasConfiguration('options.template')) { - return; + public function handle( + NodeCreationCommands $commands, + NodeCreationElements $elements + ): NodeCreationCommands { + $nodeType = $this->contentRepository->getNodeTypeManager() + ->getNodeType($commands->first->nodeTypeName); + + $templateConfiguration = $nodeType?->getOptions()['template'] ?? null; + if (!$templateConfiguration) { + return $commands; } - /** @var ContentContext $contentContext */ - $contentContext = $node->getContext(); + $subgraph = $this->contentRepository->getContentGraph($commands->first->workspaceName)->getSubgraph( + $commands->first->originDimensionSpacePoint->toDimensionSpacePoint(), + VisibilityConstraints::default() + ); $evaluationContext = [ - 'data' => $data, - // triggeringNode is deprecated and will be removed in 3.0 - 'triggeringNode' => $node, - 'site' => $contentContext->getCurrentSiteNode(), - 'parentNode' => $node->getParent(), + 'data' => iterator_to_array($elements->serialized()), + 'site' => $subgraph->findClosestNode($commands->first->parentNodeAggregateId, FindClosestNodeFilter::create(NodeTypeNameFactory::NAME_SITE)), + 'parentNode' => $subgraph->findNodeById($commands->first->parentNodeAggregateId) ]; - $templateConfiguration = $node->getNodeType()->getConfiguration('options.template'); - $processingErrors = ProcessingErrors::create(); - $template = $this->templateConfigurationProcessor->processTemplateConfiguration($templateConfiguration, $evaluationContext, $processingErrors); - $shouldContinue = $this->processingErrorHandler->handleAfterTemplateConfigurationProcessing($processingErrors, $node); + $shouldContinue = $this->processingErrorHandler->handleAfterTemplateConfigurationProcessing($processingErrors, $nodeType, $commands->first->nodeAggregateId); if (!$shouldContinue) { - return; + return $commands; } - $nodeMutators = $this->nodeCreationService->createMutatorsForRootTemplate($template, $node->getNodeType(), $this->nodeTypeManager, $node->getContext(), $processingErrors); - - $shouldContinue = $this->processingErrorHandler->handleAfterNodeCreation($processingErrors, $node); + $additionalCommands = $this->nodeCreationService->apply($template, $commands, $this->contentRepository->getNodeTypeManager(), $subgraph, $nodeType, $processingErrors); + $shouldContinue = $this->processingErrorHandler->handleAfterNodeCreation($processingErrors, $nodeType, $commands->first->nodeAggregateId); if (!$shouldContinue) { - return; + return $commands; } - $nodeMutators->executeWithStartingNode($node); + return $additionalCommands; } } diff --git a/Classes/Domain/TemplateNodeCreationHandlerFactory.php b/Classes/Domain/TemplateNodeCreationHandlerFactory.php new file mode 100644 index 0000000..a336103 --- /dev/null +++ b/Classes/Domain/TemplateNodeCreationHandlerFactory.php @@ -0,0 +1,33 @@ +nodeCreationService, + $this->templateConfigurationProcessor, + $this->processingErrorHandler + ); + } +} diff --git a/Configuration/NodeTypes.yaml b/Configuration/NodeTypes.yaml index c40158e..043f058 100644 --- a/Configuration/NodeTypes.yaml +++ b/Configuration/NodeTypes.yaml @@ -5,4 +5,4 @@ # after the default Neos.Neos.Ui creationHandler # https://github.com/Flowpack/Flowpack.NodeTemplates/pull/21 position: end - nodeCreationHandler: 'Flowpack\NodeTemplates\Domain\TemplateNodeCreationHandler' + factoryClassName: Flowpack\NodeTemplates\Domain\TemplateNodeCreationHandlerFactory diff --git a/Configuration/Testing/Settings.yaml b/Configuration/Testing/Settings.yaml new file mode 100644 index 0000000..101ba63 --- /dev/null +++ b/Configuration/Testing/Settings.yaml @@ -0,0 +1,21 @@ +Neos: + ContentRepositoryRegistry: + contentRepositories: + node_templates: + preset: default + authProvider: + factoryObjectName: Neos\ContentRepository\TestSuite\Fakes\FakeAuthProviderFactory + clock: + factoryObjectName: Neos\ContentRepository\TestSuite\Fakes\FakeClockFactory + nodeTypeManager: + factoryObjectName: Neos\ContentRepository\TestSuite\Fakes\FakeNodeTypeManagerFactory + contentDimensionSource: + factoryObjectName: Neos\ContentRepository\TestSuite\Fakes\FakeContentDimensionSourceFactory + Neos: + sites: + 'node-templates-site': + uriPathSuffix: '.html' + contentRepository: node_templates + contentDimensions: + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\AutoUriPathResolverFactory diff --git a/README.md b/README.md index 7955659..d66b267 100644 --- a/README.md +++ b/README.md @@ -164,12 +164,11 @@ There are several variables available in the EEL context for example. | data | `array` | Data from the node creation dialog | Global | | site | `Node` | The site node in which the new node be created in | Global | | parentNode | `Node` | The node where the new utmost node will be created inside | Global | -| ~triggeringNode~ | `Node` | _Deprecated:_ The new node itself which is triggering the template processing | Global | | item | `mixed` | The current item value inside a loop | Inside `withItems` loop | | key | `string` | The current item key inside a loop | Inside `withItems` loop | > **Notice** -> `triggeringNode` will be removed with version 3.0 +> `triggeringNode` was removed with version 3.0. Please use `site` or `parentNode` instead. > **Warning** > The behaviour of `parentNode` changed from version 1.x to version 2.2 @@ -240,15 +239,18 @@ It behaves similar with properties: In case a property value doesn't match its d It might be tedious to validate that all your templates are working especially in a larger project. To validate the ones that are not dependent on data from the node creation dialog (less complex templates) you can utilize this command: ```sh -flow nodetemplate:validate +flow nodetemplate:validate [] ``` +**options:** +- `--site`: the Neos site, which determines the content repository. Defaults to the first available one. + In case everything is okay it will succeed with `X NodeType templates validated.`. But in case you either have a syntax error in your template or the template does not match the node structure (illegal properties) you will be warned: ``` -76 of 78 NodeType template validated. 2 could not be build standalone. +Content repository "default": 76 of 78 NodeType template validated. 2 could not be build standalone. My.NodeType:Bing Property "someLegacyProperty" in NodeType "My.NodeType:Bing" | PropertyIgnoredException(Because property is not declared in NodeType. Got value `"bg-gray-100"`., 1685869035209) @@ -267,12 +269,13 @@ When creating a more complex node template (to create multiple pages and content For this case you can use the command: ```sh -flow nodeTemplate:createFromNodeSubtree +flow nodeTemplate:createFromNodeSubtree [] ``` - `--starting-node-id`: specified root node of the node tree **options:** +- `--site`: the Neos site, which determines the content repository. Defaults to the first available one. - `--workspace-name`: custom workspace to dump from. Defaults to 'live'. It will give you the output similar to the yaml example above. diff --git a/Tests/Functional/AbstractNodeTemplateTestCase.php b/Tests/Functional/AbstractNodeTemplateTestCase.php index 5690313..958ecc2 100644 --- a/Tests/Functional/AbstractNodeTemplateTestCase.php +++ b/Tests/Functional/AbstractNodeTemplateTestCase.php @@ -7,58 +7,66 @@ use Flowpack\NodeTemplates\Domain\NodeTemplateDumper\NodeTemplateDumper; use Flowpack\NodeTemplates\Domain\Template\RootTemplate; use Flowpack\NodeTemplates\Domain\TemplateConfiguration\TemplateConfigurationProcessor; -use Neos\ContentRepository\Domain\Model\NodeInterface; -use Neos\ContentRepository\Domain\Model\Workspace; -use Neos\ContentRepository\Domain\Repository\ContentDimensionRepository; -use Neos\ContentRepository\Domain\Repository\WorkspaceRepository; -use Neos\ContentRepository\Domain\Service\ContextFactoryInterface; -use Neos\ContentRepository\Domain\Service\NodeTypeManager; -use Neos\Flow\Tests\FunctionalTestCase; -use Neos\Neos\Domain\Model\Site; -use Neos\Neos\Domain\Repository\SiteRepository; +use Neos\Behat\FlowEntitiesTrait; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; +use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; +use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; +use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\CreateRootNodeAggregateWithNode; +use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSubtreeFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; +use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeName; +use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; +use Neos\ContentRepository\TestSuite\Fakes\FakeContentDimensionSourceFactory; +use Neos\ContentRepository\TestSuite\Fakes\FakeNodeTypeManagerFactory; +use Neos\Flow\Core\Bootstrap; +use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Neos\Ui\Domain\Model\ChangeCollection; use Neos\Neos\Ui\Domain\Model\FeedbackCollection; use Neos\Neos\Ui\TypeConverter\ChangeCollectionConverter; +use PHPUnit\Framework\TestCase; -abstract class AbstractNodeTemplateTestCase extends FunctionalTestCase +abstract class AbstractNodeTemplateTestCase extends TestCase // we don't use Flows functional test case as it would reset the database afterwards (see FlowEntitiesTrait) { use SnapshotTrait; use FeedbackCollectionMessagesTrait; use JsonSerializeNodeTreeTrait; use WithConfigurationTrait; use FakeNodeTypeManagerTrait; + use FlowEntitiesTrait; - protected static $testablePersistenceEnabled = true; + use ContentRepositoryTestTrait; - private ContextFactoryInterface $contextFactory; + protected Node $homePageNode; - protected NodeInterface $homePageNode; + protected Node $homePageMainContentCollectionNode; - protected NodeInterface $homePageMainContentCollectionNode; + private ContentSubgraphInterface $subgraph; private NodeTemplateDumper $nodeTemplateDumper; private RootTemplate $lastCreatedRootTemplate; - private NodeTypeManager $nodeTypeManager; - private string $fixturesDir; - /** @deprecated please use {@see self::getObject()} instead */ - protected $objectManager; + protected ObjectManagerInterface $objectManager; public function setUp(): void { - parent::setUp(); - - $this->nodeTypeManager = $this->objectManager->get(NodeTypeManager::class); - - $this->loadFakeNodeTypes(); + $this->objectManager = Bootstrap::$staticObjectManager; $this->setupContentRepository(); - $this->nodeTemplateDumper = $this->objectManager->get(NodeTemplateDumper::class); + $this->nodeTemplateDumper = $this->getObject(NodeTemplateDumper::class); - $templateFactory = $this->objectManager->get(TemplateConfigurationProcessor::class); + $templateFactory = $this->getObject(TemplateConfigurationProcessor::class); $templateFactoryMock = $this->getMockBuilder(TemplateConfigurationProcessor::class)->disableOriginalConstructor()->getMock(); $templateFactoryMock->expects(self::once())->method('processTemplateConfiguration')->willReturnCallback(function (...$args) use($templateFactory) { @@ -69,17 +77,13 @@ public function setUp(): void $this->objectManager->setInstance(TemplateConfigurationProcessor::class, $templateFactoryMock); $ref = new \ReflectionClass($this); - $this->fixturesDir = dirname($ref->getFileName()) . '/Snapshots'; + $this->fixturesDir = dirname($ref->getFileName() ?: '') . '/Snapshots'; } public function tearDown(): void { - parent::tearDown(); - $this->inject($this->contextFactory, 'contextInstances', []); - $this->objectManager->get(FeedbackCollection::class)->reset(); - $this->objectManager->forgetInstance(ContentDimensionRepository::class); + $this->getObject(FeedbackCollection::class)->reset(); $this->objectManager->forgetInstance(TemplateConfigurationProcessor::class); - $this->objectManager->forgetInstance(NodeTypeManager::class); } /** @@ -95,70 +99,123 @@ final protected function getObject(string $className): object private function setupContentRepository(): void { - // Create an environment to create nodes. - $this->objectManager->get(ContentDimensionRepository::class)->setDimensionsConfiguration([]); - - $liveWorkspace = new Workspace('live'); - $workspaceRepository = $this->objectManager->get(WorkspaceRepository::class); - $workspaceRepository->add($liveWorkspace); + $nodeTypeConfiguration = $this->getTestingNodeTypeConfiguration(); + FakeNodeTypeManagerFactory::setConfiguration($nodeTypeConfiguration); + FakeContentDimensionSourceFactory::setWithoutDimensions(); - $testSite = new Site('test-site'); - $testSite->setSiteResourcesPackageKey('Test.Site'); - $siteRepository = $this->objectManager->get(SiteRepository::class); - $siteRepository->add($testSite); + $this->initCleanContentRepository(ContentRepositoryId::fromString('node_templates')); + $this->truncateAndSetupFlowEntities(); - $this->persistenceManager->persistAll(); - $this->contextFactory = $this->objectManager->get(ContextFactoryInterface::class); - $subgraph = $this->contextFactory->create(['workspaceName' => 'live']); + $liveWorkspaceCommand = CreateRootWorkspace::create( + $workspaceName = WorkspaceName::fromString('live'), + ContentStreamId::fromString('cs-identifier') + ); - $rootNode = $subgraph->getRootNode(); + $this->contentRepository->handle($liveWorkspaceCommand); - $sitesRootNode = $rootNode->createNode('sites'); - $testSiteNode = $sitesRootNode->createNode('test-site'); - $this->homePageNode = $testSiteNode->createNode( - 'homepage', - $this->nodeTypeManager->getNodeType('Flowpack.NodeTemplates:Document.HomePage') + $rootNodeCommand = CreateRootNodeAggregateWithNode::create( + $workspaceName, + $sitesId = NodeAggregateId::fromString('sites'), + NodeTypeName::fromString('Neos.Neos:Sites') ); - $this->homePageMainContentCollectionNode = $this->homePageNode->getNode('main'); + $this->contentRepository->handle($rootNodeCommand); + + $siteNodeCommand = CreateNodeAggregateWithNode::create( + $workspaceName, + $testSiteId = NodeAggregateId::fromString('test-site'), + NodeTypeName::fromString('Flowpack.NodeTemplates:Document.HomePage'), + OriginDimensionSpacePoint::fromDimensionSpacePoint( + $dimensionSpacePoint = DimensionSpacePoint::fromArray([]) + ), + $sitesId, + )->withNodeName(NodeName::fromString('test-site')); + + $this->contentRepository->handle($siteNodeCommand); + + $this->subgraph = $this->contentRepository->getContentGraph($workspaceName)->getSubgraph($dimensionSpacePoint, VisibilityConstraints::withoutRestrictions()); + + $homePage = $this->subgraph->findNodeById($testSiteId); + assert($homePage instanceof Node); + $this->homePageNode = $homePage; + + $homePageMainCollection = $this->subgraph->findNodeByPath( + NodeName::fromString('main'), + $testSiteId + ); + assert($homePageMainCollection instanceof Node); + $this->homePageMainContentCollectionNode = $homePageMainCollection; + + // For the case you the Neos Site is expected to return the correct site node you can use: + + // $siteRepositoryMock = $this->getMockBuilder(SiteRepository::class)->disableOriginalConstructor()->getMock(); + // $siteRepositoryMock->expects(self::once())->method('findOneByNodeName')->willReturnCallback(function (string|SiteNodeName $nodeName) use ($testSite) { + // $nodeName = is_string($nodeName) ? SiteNodeName::fromString($nodeName) : $nodeName; + // return $nodeName->toNodeName()->equals($testSite->nodeName) + // ? $testSite + // : null; + // }); + + // or + + // $testSite = new Site($testSite->nodeName->value); + // $testSite->setSiteResourcesPackageKey('Test.Site'); + // $siteRepository = $this->objectManager->get(SiteRepository::class); + // $siteRepository->add($testSite); + // $this->persistenceManager->persistAll(); } /** - * @param NodeInterface $targetNode * @param array $nodeCreationDialogValues */ - protected function createNodeInto(NodeInterface $targetNode, string $nodeTypeName, array $nodeCreationDialogValues): NodeInterface + protected function createNodeInto(Node $targetNode, string $nodeTypeName, array $nodeCreationDialogValues): Node { - self::assertTrue($this->nodeTypeManager->hasNodeType($nodeTypeName), sprintf('NodeType %s doesnt exits.', $nodeTypeName)); - - $targetNodeContextPath = $targetNode->getContextPath(); + $targetNodeAddress = NodeAddress::fromNode($targetNode); + $serializedTargetNodeAddress = $targetNodeAddress->toJson(); - /** @see \Neos\Neos\Ui\Domain\Model\Changes\Create */ $changeCollectionSerialized = [[ 'type' => 'Neos.Neos.Ui:CreateInto', - 'subject' => $targetNodeContextPath, + 'subject' => $serializedTargetNodeAddress, 'payload' => [ - 'parentContextPath' => $targetNodeContextPath, + 'parentContextPath' => $serializedTargetNodeAddress, 'parentDomAddress' => [ - 'contextPath' => $targetNodeContextPath, + 'contextPath' => $serializedTargetNodeAddress, ], 'nodeType' => $nodeTypeName, 'name' => 'new-node', + 'nodeAggregateId' => '186b511b-b807-6208-9e1c-593e7c1a63d3', 'data' => $nodeCreationDialogValues, 'baseNodeType' => '', ], ]]; - $changeCollection = (new ChangeCollectionConverter())->convertFrom($changeCollectionSerialized, null); + $changeCollection = (new ChangeCollectionConverter())->convert($changeCollectionSerialized, $this->contentRepositoryId); assert($changeCollection instanceof ChangeCollection); $changeCollection->apply(); - return $targetNode->getNode('new-node'); + $node = $this->subgraph->findNodeByPath( + NodeName::fromString('new-node'), + $targetNode->aggregateId + ); + assert($node instanceof Node); + return $node; } - protected function createFakeNode(string $nodeAggregateId): NodeInterface + protected function createFakeNode(string $nodeAggregateId): Node { - return $this->homePageNode->createNode(uniqid('node-'), $this->nodeTypeManager->getNodeType('unstructured'), $nodeAggregateId); + $this->contentRepository->handle( + CreateNodeAggregateWithNode::create( + $this->homePageNode->workspaceName, + $someNodeId = NodeAggregateId::fromString($nodeAggregateId), + NodeTypeName::fromString('unstructured'), + $this->homePageNode->originDimensionSpacePoint, + $this->homePageNode->aggregateId, + )->withNodeName(NodeName::fromString(uniqid('node-'))) + ); + + $node = $this->subgraph->findNodeById($someNodeId); + assert($node instanceof Node); + return $node; } protected function assertLastCreatedTemplateMatchesSnapshot(string $snapShotName): void @@ -166,12 +223,12 @@ protected function assertLastCreatedTemplateMatchesSnapshot(string $snapShotName $lastCreatedTemplate = $this->serializeValuesInArray( $this->lastCreatedRootTemplate->jsonSerialize() ); - $this->assertJsonStringEqualsJsonFileOrCreateSnapshot($this->fixturesDir . '/' . $snapShotName . '.template.json', json_encode($lastCreatedTemplate, JSON_PRETTY_PRINT)); + $this->assertJsonStringEqualsJsonFileOrCreateSnapshot($this->fixturesDir . '/' . $snapShotName . '.template.json', json_encode($lastCreatedTemplate, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); } protected function assertCaughtExceptionsMatchesSnapshot(string $snapShotName): void { - $this->assertJsonStringEqualsJsonFileOrCreateSnapshot($this->fixturesDir . '/' . $snapShotName . '.messages.json', json_encode($this->getMessagesOfFeedbackCollection(), JSON_PRETTY_PRINT)); + $this->assertJsonStringEqualsJsonFileOrCreateSnapshot($this->fixturesDir . '/' . $snapShotName . '.messages.json', json_encode($this->getMessagesOfFeedbackCollection(), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); } protected function assertNoExceptionsWereCaught(): void @@ -179,15 +236,22 @@ protected function assertNoExceptionsWereCaught(): void self::assertSame([], $this->getMessagesOfFeedbackCollection()); } - protected function assertNodeDumpAndTemplateDumpMatchSnapshot(string $snapShotName, NodeInterface $node): void + protected function assertNodeDumpAndTemplateDumpMatchSnapshot(string $snapShotName, Node $node): void { - $serializedNodes = $this->jsonSerializeNodeAndDescendents($node); + $subtree = $this->subgraph->findSubtree( + $node->aggregateId, + FindSubtreeFilter::create( + nodeTypes: 'Neos.Neos:Node' + ) + ); + assert($subtree instanceof Subtree); + $serializedNodes = $this->jsonSerializeNodeAndDescendents($subtree); unset($serializedNodes['nodeTypeName']); - $this->assertJsonStringEqualsJsonFileOrCreateSnapshot($this->fixturesDir . '/' . $snapShotName . '.nodes.json', json_encode($serializedNodes, JSON_PRETTY_PRINT)); + $this->assertJsonStringEqualsJsonFileOrCreateSnapshot($this->fixturesDir . '/' . $snapShotName . '.nodes.json', json_encode($serializedNodes, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); - $dumpedYamlTemplate = $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node); + $dumpedYamlTemplate = $this->nodeTemplateDumper->createNodeTemplateYamlDumpFromSubtree($node, $this->contentRepository); - $yamlTemplateWithoutOriginNodeTypeName = '\'{nodeTypeName}\'' . substr($dumpedYamlTemplate, strlen($node->getNodeType()->getName()) + 2); + $yamlTemplateWithoutOriginNodeTypeName = '\'{nodeTypeName}\'' . substr($dumpedYamlTemplate, strlen($node->nodeTypeName->value) + 2); $this->assertStringEqualsFileOrCreateSnapshot($this->fixturesDir . '/' . $snapShotName . '.yaml', $yamlTemplateWithoutOriginNodeTypeName); } diff --git a/Tests/Functional/ContentRepositoryTestTrait.php b/Tests/Functional/ContentRepositoryTestTrait.php new file mode 100644 index 0000000..2043940 --- /dev/null +++ b/Tests/Functional/ContentRepositoryTestTrait.php @@ -0,0 +1,45 @@ + $className + * + * @return T + */ + abstract protected function getObject(string $className): object; + + private function initCleanContentRepository(ContentRepositoryId $contentRepositoryId): void + { + $this->contentRepositoryId = $contentRepositoryId; + + $contentRepositoryRegistry = $this->getObject(ContentRepositoryRegistry::class); + $contentRepositoryRegistry->resetFactoryInstance($contentRepositoryId); + + $this->contentRepository = $contentRepositoryRegistry->get($this->contentRepositoryId); + /** @var ContentRepositoryMaintainer $contentRepositoryMaintainer */ + $contentRepositoryMaintainer = $contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); + // Performance optimization: only run the setup once + if (!self::$wasContentRepositorySetupCalled) { + $contentRepositoryMaintainer->setUp(); + self::$wasContentRepositorySetupCalled = true; + } + + $contentRepositoryMaintainer->prune(); + } +} diff --git a/Tests/Functional/FakeNodeTypeManagerTrait.php b/Tests/Functional/FakeNodeTypeManagerTrait.php index 4c6157e..87bf264 100644 --- a/Tests/Functional/FakeNodeTypeManagerTrait.php +++ b/Tests/Functional/FakeNodeTypeManagerTrait.php @@ -4,14 +4,11 @@ namespace Flowpack\NodeTemplates\Tests\Functional; -use Neos\ContentRepository\Domain\Service\NodeTypeManager; +use Neos\ContentRepositoryRegistry\Configuration\NodeTypeEnrichmentService; use Neos\Flow\Configuration\ConfigurationManager; use Neos\Utility\Arrays; use Symfony\Component\Yaml\Yaml; -/** - * @property NodeTypeManager $nodeTypeManager - */ trait FakeNodeTypeManagerTrait { /** @@ -22,7 +19,7 @@ trait FakeNodeTypeManagerTrait */ abstract protected function getObject(string $className): object; - private function loadFakeNodeTypes(): void + private function getTestingNodeTypeConfiguration(): array { $configuration = $this->getObject(ConfigurationManager::class)->getConfiguration('NodeTypes'); @@ -40,6 +37,11 @@ private function loadFakeNodeTypes(): void ); } - $this->nodeTypeManager->overrideNodeTypes($configuration); + // hack, we use the service here to expand the `i18n` magic label + $finalConfiguration = $this->objectManager->get(NodeTypeEnrichmentService::class)->enrichNodeTypeLabelsConfiguration( + $configuration + ); + + return $finalConfiguration; } } diff --git a/Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.messages.json b/Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.messages.json index b687c11..e2e9758 100644 --- a/Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.messages.json +++ b/Tests/Functional/Features/ChildNodes/Snapshots/DisallowedChildNodes.messages.json @@ -1,14 +1,14 @@ [ { - "message": "Template for \"DisallowedChildNodes\" only partially applied. Please check the newly created nodes beneath Node \/sites\/test-site\/homepage\/main\/new-node@live[Flowpack.NodeTemplates:Content.DisallowedChildNodes].", + "message": "Template for \"DisallowedChildNodes\" only partially applied. Please check the newly created nodes beneath 186b511b-b807-6208-9e1c-593e7c1a63d3.", "severity": "ERROR" }, { "message": "NodeConstraintException(Node type \"Flowpack.NodeTemplates:Content.Text\" is not allowed below tethered child nodes \"content\" of nodes of type \"Flowpack.NodeTemplates:Content.DisallowedChildNodes\", 1687541480146)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "NodeConstraintException(Node type \"Flowpack.NodeTemplates:Document.Page\" is not allowed for child nodes of type Flowpack.NodeTemplates:Content.DisallowedChildNodes, 1686417627173)", - "severity": "ERROR" + "severity": "WARNING" } ] diff --git a/Tests/Functional/Features/Exceptions/Snapshots/OnlyExceptions.messages.json b/Tests/Functional/Features/Exceptions/Snapshots/OnlyExceptions.messages.json index 94ca9b1..698e39b 100644 --- a/Tests/Functional/Features/Exceptions/Snapshots/OnlyExceptions.messages.json +++ b/Tests/Functional/Features/Exceptions/Snapshots/OnlyExceptions.messages.json @@ -1,10 +1,10 @@ [ { - "message": "Template for \"OnlyExceptions\" was not applied. Only Node \/sites\/test-site\/homepage\/main\/new-node@live[Flowpack.NodeTemplates:Content.OnlyExceptions] was created.", + "message": "Template for \"OnlyExceptions\" was not applied. Only 186b511b-b807-6208-9e1c-593e7c1a63d3 was created.", "severity": "ERROR" }, { "message": "Expression \"${'left open\" in \"childNodes.abort.when\" | EelException(The EEL expression \"${'left open\" was not a valid EEL expression. Perhaps you forgot to wrap it in ${...}?, 1410441849)", - "severity": "ERROR" + "severity": "WARNING" } ] diff --git a/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.messages.json b/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.messages.json index 3e23bdd..8175488 100644 --- a/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.messages.json +++ b/Tests/Functional/Features/Exceptions/Snapshots/SomeExceptions.messages.json @@ -1,102 +1,102 @@ [ { - "message": "Template for \"SomeExceptions\" only partially applied. Please check the newly created nodes beneath Node \/sites\/test-site\/homepage\/main\/new-node@live[Flowpack.NodeTemplates:Content.SomeExceptions].", + "message": "Template for \"SomeExceptions\" only partially applied. Please check the newly created nodes beneath 186b511b-b807-6208-9e1c-593e7c1a63d3.", "severity": "ERROR" }, { "message": "Expression \"${cannotCallThis()}\" in \"properties.foo\" | NotAllowedException(Method \"cannotCallThis\" is not callable in untrusted context, 1369043080)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Expression \"${'left open\" in \"properties.bar\" | EelException(The EEL expression \"${'left open\" was not a valid EEL expression. Perhaps you forgot to wrap it in ${...}?, 1410441849)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Configuration \"properties.nonEelArrayNotAllowed\" | RuntimeException(Template configuration properties can only hold int|float|string|bool|null. Property \"nonEelArrayNotAllowed\" has type \"array\", 1685725310730)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Expression \"${parse \u00e4\u00fc\u00e4\u00f6 error}\" in \"childNodes.whenAbort.when\" | ParserException(Expression \"parse \u00e4\u00fc\u00e4\u00f6 error\" could not be parsed. Error starting at character 5: \" \u00e4\u00fc\u00e4\u00f6 error\"., 1327682383)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Expression \"${}\" in \"childNodes.withContextAbort.withContext.foo\" | ParserException(Expression \"\" could not be parsed., 1344513194)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Expression \"${Array.map()}\" in \"childNodes.withItemsAbort.withItems\" | ArgumentCountError(Too few arguments to function Neos\\Eel\\Helper\\ArrayHelper::map(), 0 passed and exactly 2 expected, 0)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Expression \"${\" in \"childNodes.propertiesPartiallyWorking.properties.propertyIsExcludedFromTemplate\" | EelException(The EEL expression \"${\" was not a valid EEL expression. Perhaps you forgot to wrap it in ${...}?, 1410441849)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Expression \"${\" in \"childNodes.typeAbort.type\" | EelException(The EEL expression \"${\" was not a valid EEL expression. Perhaps you forgot to wrap it in ${...}?, 1410441849)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Expression \"${\" in \"childNodes.nameAbort.name\" | EelException(The EEL expression \"${\" was not a valid EEL expression. Perhaps you forgot to wrap it in ${...}?, 1410441849)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Expression \"${item == 1 ? cannotCallThis() : null}\" in \"childNodes.withItemsPartiallyWorking.name\" | NotAllowedException(Method \"cannotCallThis\" is not callable in untrusted context, 1369043080)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Configuration \"childNodes.withItemsAbortBecauseNotIterable.withItems\" | RuntimeException(Type NULL is not iterable., 1685802354186)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Configuration \"childNodes.invalidOption.crazy\" | InvalidArgumentException(Template configuration has illegal key \"crazy\", 1686150349274)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Property \"_hidden\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | PropertyIgnoredException(Because internal legacy property \"_hidden\" not implement., 1686149513158)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Property \"_hiddenAfterDateTime\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | PropertyIgnoredException(Because internal legacy property \"_hiddenAfterDateTime\" not implement., 1686149513158)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Property \"boolValue\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | PropertyIgnoredException(Because value `123` is not assignable to property type \"boolean\"., 1685958105644)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Property \"stringValue\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | PropertyIgnoredException(Because value `false` is not assignable to property type \"string\"., 1685958105644)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Property \"nonDeclaredProperty\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | PropertyIgnoredException(Because property is not declared in NodeType. Got value `\"hi\"`., 1685869035209)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Reference \"reference\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | InvalidReferenceException(Node with identifier \"non-existing-node-id\" does not exist., 1687632330292)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Reference \"references\" in NodeType \"Flowpack.NodeTemplates:Content.SomeExceptions\" | InvalidReferenceException(Node with identifier \"non-existing-node-id\" does not exist., 1687632330292)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "RuntimeException(Template requires type to be a non abstract NodeType. Got: \"Neos.Neos:Node\"., 1686417628976)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "NodeConstraintException(Node type \"Flowpack.NodeTemplates:Document.Page\" is not allowed for child nodes of type Flowpack.NodeTemplates:Content.SomeExceptions, 1686417627173)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "RuntimeException(Template requires type to be set for non auto created child nodes., 1685999829307)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "RuntimeException(Template requires type to be a valid NodeType. Got: \"Flowpack.NodeTemplates:InvalidNodeType\"., 1685999795564)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "RuntimeException(Template cant mutate type of auto created child nodes. Got: \"Flowpack.NodeTemplates:Content.SomeExceptions\", 1685999829307)", - "severity": "ERROR" + "severity": "WARNING" } ] diff --git a/Tests/Functional/Features/NodeTypes.yaml b/Tests/Functional/Features/NodeTypes.yaml index 5b1af7d..7c0407b 100644 --- a/Tests/Functional/Features/NodeTypes.yaml +++ b/Tests/Functional/Features/NodeTypes.yaml @@ -2,7 +2,7 @@ --- 'Flowpack.NodeTemplates:Document.HomePage': superTypes: - 'Neos.Neos:Document': true + 'Neos.Neos:Site': true constraints: nodeTypes: unstructured: true @@ -35,3 +35,8 @@ constraints: nodeTypes: '*': false + +'unstructured': {} + +# todo remove me +'Neos.ContentRepository:Root': {} diff --git a/Tests/Functional/Features/Properties/Snapshots/Properties.nodes.json b/Tests/Functional/Features/Properties/Snapshots/Properties.nodes.json index 544d30d..0b6aa98 100644 --- a/Tests/Functional/Features/Properties/Snapshots/Properties.nodes.json +++ b/Tests/Functional/Features/Properties/Snapshots/Properties.nodes.json @@ -1,6 +1,5 @@ { "properties": { - "unsetValueWithDefault": null, "someValueWithDefault": true, "text": "abc", "isEnchanted": false, diff --git a/Tests/Functional/Features/Properties/Snapshots/Properties.yaml b/Tests/Functional/Features/Properties/Snapshots/Properties.yaml index fe79027..8030f5d 100644 --- a/Tests/Functional/Features/Properties/Snapshots/Properties.yaml +++ b/Tests/Functional/Features/Properties/Snapshots/Properties.yaml @@ -9,4 +9,4 @@ # Select Box # selectBox -> SelectBox of ["karma","longLive"] with value "karma" # Reference - # reference -> Reference of NodeTypes (Flowpack.NodeTemplates:Content.Properties) with value Node(7f7bac1c-9400-4db5-bbaa-2b8251d127c5) + # reference -> Reference of NodeTypes (Flowpack.NodeTemplates:Content.Properties) with Node: 7f7bac1c-9400-4db5-bbaa-2b8251d127c5 diff --git a/Tests/Functional/Features/ResolvableProperties/Snapshots/ResolvableProperties.yaml b/Tests/Functional/Features/ResolvableProperties/Snapshots/ResolvableProperties.yaml index 63c8393..740bb4b 100644 --- a/Tests/Functional/Features/ResolvableProperties/Snapshots/ResolvableProperties.yaml +++ b/Tests/Functional/Features/ResolvableProperties/Snapshots/ResolvableProperties.yaml @@ -4,5 +4,5 @@ properties: # asset -> object(Neos\Media\Domain\Model\Asset) # images -> array(Neos\Media\Domain\Model\Image) - # reference -> Reference of NodeTypes (Neos.Neos:Document) with value Node(some-node-id) - # references -> Nodes(some-node-id, other-node-id, real-node-id) + # reference -> Reference of NodeTypes (Neos.Neos:Document) with Node: some-node-id + # references -> References with Nodes: some-node-id, other-node-id, real-node-id diff --git a/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.messages.json b/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.messages.json index 7d90e52..2116ea1 100644 --- a/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.messages.json +++ b/Tests/Functional/Features/ResolvableProperties/Snapshots/UnresolvableProperties.messages.json @@ -1,26 +1,26 @@ [ { - "message": "Template for \"UnresolvableProperties\" only partially applied. Please check the newly created nodes beneath Node \/sites\/test-site\/homepage\/main\/new-node@live[Flowpack.NodeTemplates:Content.UnresolvableProperties].", + "message": "Template for \"UnresolvableProperties\" only partially applied. Please check the newly created nodes beneath 186b511b-b807-6208-9e1c-593e7c1a63d3.", "severity": "ERROR" }, { "message": "Property \"someString\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | PropertyIgnoredException(Because value `[\"foo\"]` is not assignable to property type \"string\"., 1685958105644)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Property \"asset\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | PropertyIgnoredException(Object of type \"Neos\\Media\\Domain\\Model\\Asset\" with identity \"non-existing\" not found., 1686779371122)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Property \"images\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | FlowException(Could not convert target type \"array\", at property path \"0\": No converter found which can be used to convert from \"string\" to \"Neos\\Media\\Domain\\Model\\ImageInterface\"., 1297759968) | TypeConverterException(No converter found which can be used to convert from \"string\" to \"Neos\\Media\\Domain\\Model\\ImageInterface\"., 0)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Reference \"reference\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | InvalidReferenceException(Invalid reference value. Value `true` is not a valid node or node identifier., 1687632177555)", - "severity": "ERROR" + "severity": "WARNING" }, { "message": "Reference \"references\" in NodeType \"Flowpack.NodeTemplates:Content.UnresolvableProperties\" | InvalidReferenceException(Node with identifier \"some-non-existing-node-id\" does not exist., 1687632330292)", - "severity": "ERROR" + "severity": "WARNING" } ] diff --git a/Tests/Functional/Features/StandaloneValidationCommand/Snapshots/NodeTemplateValidateOutput.log b/Tests/Functional/Features/StandaloneValidationCommand/Snapshots/NodeTemplateValidateOutput.log index e4e0f03..028e345 100644 --- a/Tests/Functional/Features/StandaloneValidationCommand/Snapshots/NodeTemplateValidateOutput.log +++ b/Tests/Functional/Features/StandaloneValidationCommand/Snapshots/NodeTemplateValidateOutput.log @@ -1,4 +1,4 @@ -10 of 15 NodeType template validated. 5 could not be build standalone. +Content repository "node_templates": 10 of 15 NodeType template validated. 5 could not be build standalone. Flowpack.NodeTemplates:Content.DisallowedChildNodes NodeConstraintException(Node type "Flowpack.NodeTemplates:Content.Text" is not allowed below tethered child nodes "content" of nodes of type "Flowpack.NodeTemplates:Content.DisallowedChildNodes", 1687541480146) diff --git a/Tests/Functional/Features/StandaloneValidationCommand/StandaloneValidationCommandTest.php b/Tests/Functional/Features/StandaloneValidationCommand/StandaloneValidationCommandTest.php index 653b509..d37ec1a 100644 --- a/Tests/Functional/Features/StandaloneValidationCommand/StandaloneValidationCommandTest.php +++ b/Tests/Functional/Features/StandaloneValidationCommand/StandaloneValidationCommandTest.php @@ -5,56 +5,63 @@ namespace Flowpack\NodeTemplates\Tests\Functional\Features\StandaloneValidationCommand; use Flowpack\NodeTemplates\Application\Command\NodeTemplateCommandController; +use Flowpack\NodeTemplates\Tests\Functional\ContentRepositoryTestTrait; use Flowpack\NodeTemplates\Tests\Functional\FakeNodeTypeManagerTrait; use Flowpack\NodeTemplates\Tests\Functional\SnapshotTrait; -use Neos\ContentRepository\Domain\Model\Workspace; -use Neos\ContentRepository\Domain\Repository\ContentDimensionRepository; -use Neos\ContentRepository\Domain\Repository\WorkspaceRepository; -use Neos\ContentRepository\Domain\Service\ContextFactoryInterface; -use Neos\ContentRepository\Domain\Service\NodeTypeManager; +use Neos\Behat\FlowEntitiesTrait; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; +use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; +use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; +use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\CreateRootNodeAggregateWithNode; +use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeName; +use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; +use Neos\ContentRepository\TestSuite\Fakes\FakeContentDimensionSourceFactory; +use Neos\ContentRepository\TestSuite\Fakes\FakeNodeTypeManagerFactory; use Neos\Flow\Cli\Exception\StopCommandException; use Neos\Flow\Cli\Response; -use Neos\Flow\Tests\FunctionalTestCase; +use Neos\Flow\Core\Bootstrap; +use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Ui\Domain\Model\FeedbackCollection; use Neos\Utility\ObjectAccess; +use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Output\BufferedOutput; -final class StandaloneValidationCommandTest extends FunctionalTestCase +final class StandaloneValidationCommandTest extends TestCase // we don't use Flows functional test case as it would reset the database afterwards (see FlowEntitiesTrait) { use SnapshotTrait; + use ContentRepositoryTestTrait; use FakeNodeTypeManagerTrait; + use FlowEntitiesTrait; - protected static $testablePersistenceEnabled = true; - - private ContextFactoryInterface $contextFactory; - - private NodeTypeManager $nodeTypeManager; + /** + * Matching configuration in Neos.Neos.sites.node-templates-site + */ + private const TEST_SITE_NAME = 'node-templates-site'; private string $fixturesDir; + protected ObjectManagerInterface $objectManager; + public function setUp(): void { - parent::setUp(); - - $this->nodeTypeManager = $this->getObject(NodeTypeManager::class); - - $this->loadFakeNodeTypes(); + $this->objectManager = Bootstrap::$staticObjectManager; $this->setupContentRepository(); $ref = new \ReflectionClass($this); - $this->fixturesDir = dirname($ref->getFileName()) . '/Snapshots'; + $this->fixturesDir = dirname($ref->getFileName() ?: '') . '/Snapshots'; } public function tearDown(): void { - parent::tearDown(); - $this->inject($this->contextFactory, 'contextInstances', []); - $this->getObject(FeedbackCollection::class)->reset(); - $this->objectManager->forgetInstance(ContentDimensionRepository::class); - $this->objectManager->forgetInstance(NodeTypeManager::class); + $this->objectManager->get(FeedbackCollection::class)->reset(); } /** @@ -70,42 +77,64 @@ final protected function getObject(string $className): object private function setupContentRepository(): void { - // Create an environment to create nodes. - $this->getObject(ContentDimensionRepository::class)->setDimensionsConfiguration([]); + $nodeTypeConfiguration = $this->getTestingNodeTypeConfiguration(); + FakeNodeTypeManagerFactory::setConfiguration($nodeTypeConfiguration); + FakeContentDimensionSourceFactory::setWithoutDimensions(); - $liveWorkspace = new Workspace('live'); - $workspaceRepository = $this->getObject(WorkspaceRepository::class); - $workspaceRepository->add($liveWorkspace); - - $testSite = new Site('test-site'); - $testSite->setSiteResourcesPackageKey('Test.Site'); - $siteRepository = $this->getObject(SiteRepository::class); - $siteRepository->add($testSite); + $this->initCleanContentRepository(ContentRepositoryId::fromString('node_templates')); + $this->truncateAndSetupFlowEntities(); - $this->persistenceManager->persistAll(); - $this->contextFactory = $this->getObject(ContextFactoryInterface::class); - $subgraph = $this->contextFactory->create(['workspaceName' => 'live']); + $liveWorkspaceCommand = CreateRootWorkspace::create( + $workspaceName = WorkspaceName::fromString('live'), + ContentStreamId::fromString('cs-identifier') + ); - $rootNode = $subgraph->getRootNode(); + $this->contentRepository->handle($liveWorkspaceCommand); - $sitesRootNode = $rootNode->createNode('sites'); - $testSiteNode = $sitesRootNode->createNode('test-site'); - $testSiteNode->createNode( - 'homepage', - $this->nodeTypeManager->getNodeType('Flowpack.NodeTemplates:Document.HomePage') + $rootNodeCommand = CreateRootNodeAggregateWithNode::create( + $workspaceName, + $sitesId = NodeAggregateId::fromString('sites'), + NodeTypeName::fromString('Neos.Neos:Sites') ); + + $this->contentRepository->handle($rootNodeCommand); + + $siteNodeCommand = CreateNodeAggregateWithNode::create( + $workspaceName, + NodeAggregateId::fromString('test-site'), + NodeTypeName::fromString('Flowpack.NodeTemplates:Document.HomePage'), + OriginDimensionSpacePoint::fromDimensionSpacePoint( + DimensionSpacePoint::fromArray([]) + ), + $sitesId, + )->withNodeName(NodeName::fromString(self::TEST_SITE_NAME)); + + $this->contentRepository->handle($siteNodeCommand); } /** @test */ - public function itMatchesSnapshot() + public function itMatchesSnapshot(): void { $commandController = $this->getObject(NodeTemplateCommandController::class); + $testSite = new Site(self::TEST_SITE_NAME); + $testSite->setSiteResourcesPackageKey('Test.Site'); + + $siteRepositoryMock = $this->getMockBuilder(SiteRepository::class)->disableOriginalConstructor()->getMock(); + $siteRepositoryMock->expects(self::once())->method('findOneByNodeName')->willReturnCallback(function (string $nodeName) use ($testSite) { + return $nodeName === $testSite->getNodeName()->value + ? $testSite + : null; + }); + + ObjectAccess::setProperty($commandController, 'siteRepository', $siteRepositoryMock, true); + + ObjectAccess::setProperty($commandController, 'response', $cliResponse = new Response(), true); ObjectAccess::getProperty($commandController, 'output', true)->setOutput($bufferedOutput = new BufferedOutput()); try { - $commandController->validateCommand(); + $commandController->validateCommand(self::TEST_SITE_NAME); } catch (StopCommandException $e) { } diff --git a/Tests/Functional/Features/Variables/NodeTypes.Variables.yaml b/Tests/Functional/Features/Variables/NodeTypes.Variables.yaml index e10221b..1400829 100644 --- a/Tests/Functional/Features/Variables/NodeTypes.Variables.yaml +++ b/Tests/Functional/Features/Variables/NodeTypes.Variables.yaml @@ -10,4 +10,4 @@ options: template: properties: - text: "${'parentNode(' + parentNode.nodeType.name + ', ' + parentNode.name + ') site(' + site.nodeType.name + ', ' + site.name + ')'}" + text: "${'parentNode(' + parentNode.nodeTypeName.value + ', ' + parentNode.name.value + ') site(' + site.nodeTypeName.value + ', ' + site.name.value + ')'}" diff --git a/Tests/Functional/Features/Variables/Snapshots/Variables.nodes.json b/Tests/Functional/Features/Variables/Snapshots/Variables.nodes.json index 4edee45..9b03fc7 100644 --- a/Tests/Functional/Features/Variables/Snapshots/Variables.nodes.json +++ b/Tests/Functional/Features/Variables/Snapshots/Variables.nodes.json @@ -1,5 +1,5 @@ { "properties": { - "text": "parentNode(Neos.Neos:ContentCollection, main) site(unstructured, test-site)" + "text": "parentNode(Neos.Neos:ContentCollection, main) site(Flowpack.NodeTemplates:Document.HomePage, test-site)" } } diff --git a/Tests/Functional/Features/Variables/Snapshots/Variables.template.json b/Tests/Functional/Features/Variables/Snapshots/Variables.template.json index 3602f63..0762b44 100644 --- a/Tests/Functional/Features/Variables/Snapshots/Variables.template.json +++ b/Tests/Functional/Features/Variables/Snapshots/Variables.template.json @@ -1,6 +1,6 @@ { "properties": { - "text": "parentNode(Neos.Neos:ContentCollection, main) site(unstructured, test-site)" + "text": "parentNode(Neos.Neos:ContentCollection, main) site(Flowpack.NodeTemplates:Document.HomePage, test-site)" }, "childNodes": [] } diff --git a/Tests/Functional/Features/Variables/Snapshots/Variables.yaml b/Tests/Functional/Features/Variables/Snapshots/Variables.yaml index a884e83..45a2ec8 100644 --- a/Tests/Functional/Features/Variables/Snapshots/Variables.yaml +++ b/Tests/Functional/Features/Variables/Snapshots/Variables.yaml @@ -2,4 +2,4 @@ options: template: properties: - text: 'parentNode(Neos.Neos:ContentCollection, main) site(unstructured, test-site)' + text: 'parentNode(Neos.Neos:ContentCollection, main) site(Flowpack.NodeTemplates:Document.HomePage, test-site)' diff --git a/Tests/Functional/FeedbackCollectionMessagesTrait.php b/Tests/Functional/FeedbackCollectionMessagesTrait.php index bf56190..d3ea32d 100644 --- a/Tests/Functional/FeedbackCollectionMessagesTrait.php +++ b/Tests/Functional/FeedbackCollectionMessagesTrait.php @@ -18,6 +18,9 @@ trait FeedbackCollectionMessagesTrait */ abstract protected function getObject(string $className): object; + /** + * @return array + */ private function getMessagesOfFeedbackCollection(): array { /** @var FeedbackInterface[] $allFeedbacks */ diff --git a/Tests/Functional/JsonSerializeNodeTreeTrait.php b/Tests/Functional/JsonSerializeNodeTreeTrait.php index 6cfa74d..0fe5fc3 100644 --- a/Tests/Functional/JsonSerializeNodeTreeTrait.php +++ b/Tests/Functional/JsonSerializeNodeTreeTrait.php @@ -2,50 +2,66 @@ namespace Flowpack\NodeTemplates\Tests\Functional; -use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindReferencesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; +use Neos\Neos\Domain\SubtreeTagging\NeosSubtreeTag; use Neos\Utility\ObjectAccess; trait JsonSerializeNodeTreeTrait { - private function jsonSerializeNodeAndDescendents(NodeInterface $node): array + private readonly ContentRepository $contentRepository; + + /** + * @return array + */ + private function jsonSerializeNodeAndDescendents(Subtree $subtree): array { - $nodeType = $node->getNodeType(); - $references = []; - $properties = []; - foreach ($node->getProperties() as $propertyName => $propertyValue) { - $declaration = $nodeType->getPropertyType($propertyName); - if ($declaration === 'reference' || $declaration === 'references') { - $references[$propertyName] = []; - foreach ($declaration === 'reference' ? [$propertyValue] : $propertyValue as $reference) { - $references[$propertyName][] = array_filter([ - 'node' => $reference, - 'properties' => [] - ]); - } - continue; - } - $properties[$propertyName] = $propertyValue; + $node = $subtree->node; + + $subgraph = $this->contentRepository->getContentGraph($node->workspaceName)->getSubgraph( + $node->dimensionSpacePoint, + $node->visibilityConstraints + ); + + $references = $subgraph->findReferences($node->aggregateId, FindReferencesFilter::create()); + + $referencesArray = []; + foreach ($references as $reference) { + $referencesArray[$reference->name->value] ??= []; + $referencesArray[$reference->name->value][] = array_filter([ + 'node' => sprintf('Node(%s, %s)', $reference->node->aggregateId->value, $reference->node->nodeTypeName->value), + 'properties' => iterator_to_array($reference->properties ?? []) + ]); } + return array_filter([ - 'nodeTypeName' => $node->getNodeType()->getName(), - 'nodeName' => $node->isAutoCreated() ? $node->getName() : null, - 'isDisabled' => $node->isHidden(), - 'properties' => $this->serializeValuesInArray($properties), - 'references' => $this->serializeValuesInArray($references), + 'nodeTypeName' => $node->nodeTypeName, + 'nodeName' => $node->classification->isTethered() ? $node->name : null, + 'isDisabled' => $node->tags->contain(NeosSubtreeTag::disabled()), + 'properties' => $this->serializeValuesInArray( + iterator_to_array($node->properties->getIterator()) + ), + 'references' => $referencesArray, 'childNodes' => array_map( - fn ($node) => $this->jsonSerializeNodeAndDescendents($node), - $node->getChildNodes('Neos.Neos:Node') + fn ($subtree) => $this->jsonSerializeNodeAndDescendents($subtree), + iterator_to_array($subtree->children) ) ]); } + /** + * @param array $array + * @return array + */ private function serializeValuesInArray(array $array): array { foreach ($array as $key => $value) { if (is_array($value)) { $value = $this->serializeValuesInArray($value); - } elseif ($value instanceof NodeInterface) { - $value = sprintf('Node(%s, %s)', $value->getIdentifier(), $value->getNodeType()->getName()); + } elseif ($value instanceof Node) { + $value = sprintf('Node(%s, %s)', $value->aggregateId->value, $value->nodeTypeName->value); } elseif ($value instanceof \JsonSerializable) { $value = $value->jsonSerialize(); if (is_array($value)) { diff --git a/Tests/Functional/WithConfigurationTrait.php b/Tests/Functional/WithConfigurationTrait.php index 7469708..a962c94 100644 --- a/Tests/Functional/WithConfigurationTrait.php +++ b/Tests/Functional/WithConfigurationTrait.php @@ -13,7 +13,7 @@ trait WithConfigurationTrait * WARNING: If you activate Singletons during this transaction they will later still have a reference to the mocked object manger, so you might need to call * {@see ObjectManagerInterface::forgetInstance()}. An alternative would be also to hack the protected $this->settings of the manager. * - * @param array $additionalSettings settings that are merged onto the the current testing configuration + * @param array $additionalSettings settings that are merged onto the the current testing configuration * @param callable $fn test code that is executed in the modified context */ private function withMockedConfigurationSettings(array $additionalSettings, callable $fn): void diff --git a/Tests/Unit/Domain/NodeCreation/PropertyTypeTest.php b/Tests/Unit/Domain/NodeCreation/PropertyTypeTest.php index 2546db3..23f934e 100644 --- a/Tests/Unit/Domain/NodeCreation/PropertyTypeTest.php +++ b/Tests/Unit/Domain/NodeCreation/PropertyTypeTest.php @@ -14,7 +14,8 @@ use Flowpack\NodeTemplates\Domain\NodeCreation\PropertyType; use Flowpack\NodeTemplates\Tests\Unit\Domain\NodeCreation\Fixture\PostalAddress; use GuzzleHttp\Psr7\Uri; -use Neos\ContentRepository\Domain\Model\NodeType; +use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\Flow\ResourceManagement\PersistentResource; use Neos\Media\Domain\Model\Asset; use Neos\Media\Domain\Model\Image; @@ -35,12 +36,24 @@ class PropertyTypeTest extends TestCase { /** * @dataProvider declarationAndValueProvider + * @param array $declarationsByType, + * @param array $validValues, + * @param array $invalidValues, */ public function testIsMatchedBy(array $declarationsByType, array $validValues, array $invalidValues): void { foreach ($declarationsByType as $declaration) { - $nodeTypeMock = $this->getMockBuilder(NodeType::class)->disableOriginalConstructor()->getMock(); - $nodeTypeMock->expects(self::once())->method('getPropertyType')->with('test')->willReturn($declaration); + $nodeTypeMock = new NodeType( + NodeTypeName::fromString('Foo:Bar'), + [], + [ + 'properties' => [ + 'test' => [ + 'type' => $declaration, + ] + ] + ] + ); $subject = PropertyType::fromPropertyOfNodeType( 'test', $nodeTypeMock, @@ -54,6 +67,9 @@ public function testIsMatchedBy(array $declarationsByType, array $validValues, a } } + /** + * @return array> + */ public function declarationAndValueProvider(): array { $bool = true; @@ -138,14 +154,23 @@ public function declarationAndValueProvider(): array /** * @dataProvider declarationTypeProvider - * @param array $declaredTypes + * @param array $declaredTypes * @param string $expectedSerializationType */ public function testGetValue(array $declaredTypes, string $expectedSerializationType): void { foreach ($declaredTypes as $declaredType) { - $nodeTypeMock = $this->getMockBuilder(NodeType::class)->disableOriginalConstructor()->getMock(); - $nodeTypeMock->expects(self::once())->method('getPropertyType')->with('test')->willReturn($declaredType); + $nodeTypeMock = new NodeType( + NodeTypeName::fromString('Foo:Bar'), + [], + [ + 'properties' => [ + 'test' => [ + 'type' => $declaredType, + ] + ] + ] + ); $subject = PropertyType::fromPropertyOfNodeType( 'test', $nodeTypeMock, @@ -159,6 +184,9 @@ public function testGetValue(array $declaredTypes, string $expectedSerialization } } + /** + * @return array> + */ public function declarationTypeProvider(): array { return [ diff --git a/Tests/Unit/Domain/NodeCreation/ReferenceTypeTest.php b/Tests/Unit/Domain/NodeCreation/ReferenceTypeTest.php index a1007f3..6505866 100644 --- a/Tests/Unit/Domain/NodeCreation/ReferenceTypeTest.php +++ b/Tests/Unit/Domain/NodeCreation/ReferenceTypeTest.php @@ -6,9 +6,11 @@ use Flowpack\NodeTemplates\Domain\NodeCreation\InvalidReferenceException; use Flowpack\NodeTemplates\Domain\NodeCreation\ReferenceType; +use Flowpack\NodeTemplates\Tests\Unit\NodeMockTrait; use GuzzleHttp\Psr7\Uri; -use Neos\ContentRepository\Domain\Model\NodeInterface; -use Neos\ContentRepository\Domain\Model\NodeType; +use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\Flow\ResourceManagement\PersistentResource; use Neos\Media\Domain\Model\Asset; use Neos\Media\Domain\Model\Image; @@ -16,16 +18,29 @@ class ReferenceTypeTest extends TestCase { + use NodeMockTrait; + private const VALID_NODE_ID_1 = '123'; private const VALID_NODE_ID_2 = '456'; /** * @dataProvider declarationAndValueProvider + * @param array $validValues, + * @param array $invalidValues */ public function testIsMatchedBy(string $declarationType, array $validValues, array $invalidValues): void { - $nodeTypeMock = $this->getMockBuilder(NodeType::class)->disableOriginalConstructor()->getMock(); - $nodeTypeMock->expects(self::once())->method('getPropertyType')->with('test')->willReturn($declarationType); + $nodeTypeMock = new NodeType( + NodeTypeName::fromString('Foo:Bar'), + [], + [ + 'properties' => [ + 'test' => [ + 'type' => $declarationType, + ] + ] + ] + ); $subject = ReferenceType::fromPropertyOfNodeType( 'test', $nodeTypeMock, @@ -44,6 +59,9 @@ public function testIsMatchedBy(string $declarationType, array $validValues, arr } } + /** + * @return array> + */ public function declarationAndValueProvider(): array { $int = 13; @@ -55,11 +73,8 @@ public function declarationAndValueProvider(): array $date = \DateTimeImmutable::createFromFormat(\DateTimeInterface::W3C, '2020-08-20T18:56:15+00:00'); $uri = new Uri('https://www.neos.io'); - $nodeMock1 = $this->getMockBuilder(NodeInterface::class)->getMock(); - $nodeMock1->method('getIdentifier')->willReturn(self::VALID_NODE_ID_1); - - $nodeMock2 = $this->getMockBuilder(NodeInterface::class)->getMock(); - $nodeMock2->method('getIdentifier')->willReturn(self::VALID_NODE_ID_2); + $nodeMock1 = $this->createNodeMock(NodeAggregateId::fromString(self::VALID_NODE_ID_1)); + $nodeMock2 = $this->createNodeMock(NodeAggregateId::fromString(self::VALID_NODE_ID_2)); return [ [ diff --git a/Tests/Unit/Domain/NodeCreation/TransientNodeTest.php b/Tests/Unit/Domain/NodeCreation/TransientNodeTest.php index abcb4db..6fa936f 100644 --- a/Tests/Unit/Domain/NodeCreation/TransientNodeTest.php +++ b/Tests/Unit/Domain/NodeCreation/TransientNodeTest.php @@ -6,11 +6,14 @@ use Flowpack\NodeTemplates\Domain\NodeCreation\NodeConstraintException; use Flowpack\NodeTemplates\Domain\NodeCreation\TransientNode; -use Neos\ContentRepository\Domain\Model\NodeType; -use Neos\ContentRepository\Domain\NodeAggregate\NodeName; -use Neos\ContentRepository\Domain\Service\Context; -use Neos\ContentRepository\Domain\Service\NodeTypeManager; -use Neos\Utility\ObjectAccess; +use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; +use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths; +use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\NodeType\NodeTypeManager; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeName; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use PHPUnit\Framework\TestCase; use Symfony\Component\Yaml\Yaml; @@ -69,15 +72,14 @@ class TransientNodeTest extends TestCase public function setUp(): void { parent::setUp(); - $this->nodeTypeManager = new NodeTypeManager(); - $this->nodeTypeManager->overrideNodeTypes(Yaml::parse(self::NODE_TYPE_FIXTURES)); + $this->nodeTypeManager = NodeTypeManager::createFromArrayConfiguration(Yaml::parse(self::NODE_TYPE_FIXTURES)); } /** @test */ public function fromRegularAllowedChildNode(): void { $parentNode = $this->createFakeRegularTransientNode('A:Content1'); - self::assertSame($this->getNodeType('A:Content1'), $parentNode->getNodeType()); + self::assertSame($this->getNodeType('A:Content1'), $parentNode->nodeType); $parentNode->requireConstraintsImposedByAncestorsToBeMet($this->getNodeType('A:Content2')); } @@ -87,7 +89,7 @@ public function forTetheredChildNodeAllowedChildNode(): void $grandParentNode = $this->createFakeRegularTransientNode('A:WithContent1AllowedCollectionAsChildNode'); $parentNode = $grandParentNode->forTetheredChildNode(NodeName::fromString('collection'), []); - self::assertSame($this->getNodeType('A:Collection.Allowed'), $parentNode->getNodeType()); + self::assertSame($this->getNodeType('A:Collection.Allowed'), $parentNode->nodeType); $parentNode->requireConstraintsImposedByAncestorsToBeMet($this->getNodeType('A:Content1')); } @@ -98,7 +100,7 @@ public function forTetheredChildNodeAllowedChildNodeBecauseConstraintOverride(): $grandParentNode = $this->createFakeRegularTransientNode('A:WithContent1AllowedCollectionAsChildNodeViaOverride'); $parentNode = $grandParentNode->forTetheredChildNode(NodeName::fromString('collection'), []); - self::assertSame($this->getNodeType('A:Collection.Disallowed'), $parentNode->getNodeType()); + self::assertSame($this->getNodeType('A:Collection.Disallowed'), $parentNode->nodeType); $parentNode->requireConstraintsImposedByAncestorsToBeMet($this->getNodeType('A:Content1')); } @@ -108,8 +110,8 @@ public function forRegularChildNodeAllowedChildNode(): void { $grandParentNode = $this->createFakeRegularTransientNode('A:Content1'); - $parentNode = $grandParentNode->forRegularChildNode($this->getNodeType('A:Content2'), []); - self::assertSame($this->getNodeType('A:Content2'), $parentNode->getNodeType()); + $parentNode = $grandParentNode->forRegularChildNode(NodeAggregateId::fromString('child'), $this->getNodeType('A:Content2'), []); + self::assertSame($this->getNodeType('A:Content2'), $parentNode->nodeType); $parentNode->requireConstraintsImposedByAncestorsToBeMet($this->getNodeType('A:Content3')); } @@ -121,7 +123,7 @@ public function fromRegularDisallowedChildNode(): void $this->expectExceptionMessage('Node type "A:Content1" is not allowed for child nodes of type A:Collection.Disallowed'); $parentNode = $this->createFakeRegularTransientNode('A:Collection.Disallowed'); - self::assertSame($this->getNodeType('A:Collection.Disallowed'), $parentNode->getNodeType()); + self::assertSame($this->getNodeType('A:Collection.Disallowed'), $parentNode->nodeType); $parentNode->requireConstraintsImposedByAncestorsToBeMet($this->getNodeType('A:Content1')); } @@ -135,7 +137,7 @@ public function forTetheredChildNodeDisallowedChildNode(): void $grandParentNode = $this->createFakeRegularTransientNode('A:WithDisallowedCollectionAsChildNode'); $parentNode = $grandParentNode->forTetheredChildNode(NodeName::fromString('collection'), []); - self::assertSame($this->getNodeType('A:Collection.Disallowed'), $parentNode->getNodeType()); + self::assertSame($this->getNodeType('A:Collection.Disallowed'), $parentNode->nodeType); $parentNode->requireConstraintsImposedByAncestorsToBeMet($this->getNodeType('A:Content1')); } @@ -148,8 +150,8 @@ public function forRegularChildNodeDisallowedChildNode(): void $grandParentNode = $this->createFakeRegularTransientNode('A:Content2'); - $parentNode = $grandParentNode->forRegularChildNode($this->getNodeType('A:Collection.Disallowed'), []); - self::assertSame($this->getNodeType('A:Collection.Disallowed'), $parentNode->getNodeType()); + $parentNode = $grandParentNode->forRegularChildNode(NodeAggregateId::fromString('child'), $this->getNodeType('A:Collection.Disallowed'), []); + self::assertSame($this->getNodeType('A:Collection.Disallowed'), $parentNode->nodeType); $parentNode->requireConstraintsImposedByAncestorsToBeMet($this->getNodeType('A:Content1')); } @@ -157,10 +159,15 @@ public function forRegularChildNodeDisallowedChildNode(): void /** @test */ public function splitPropertiesAndReferencesByTypeDeclaration(): void { + $nodeType = $this->getNodeType('A:ContentWithProperties'); $node = TransientNode::forRegular( - $this->getNodeType('A:ContentWithProperties'), - $this->nodeTypeManager, - $this->getMockBuilder(Context::class)->disableOriginalConstructor()->getMock(), + NodeAggregateId::fromString('na'), + WorkspaceName::fromString('ws'), + OriginDimensionSpacePoint::fromArray([]), + $nodeType, + NodeAggregateIdsByNodePaths::createEmpty(), + NodeTypeManager::createFromArrayConfiguration([]), + $this->getMockBuilder(ContentSubgraphInterface::class)->disableOriginalConstructor()->getMock(), [ 'property-string' => '', 'property-integer' => '', @@ -176,7 +183,7 @@ public function splitPropertiesAndReferencesByTypeDeclaration(): void 'property-integer' => '', 'undeclared-property' => '' ], - $node->getProperties() + $node->properties ); self::assertSame( @@ -184,7 +191,7 @@ public function splitPropertiesAndReferencesByTypeDeclaration(): void 'property-reference' => '', 'property-references' => '', ], - $node->getReferences() + $node->references ); } @@ -193,9 +200,13 @@ private function createFakeRegularTransientNode(string $nodeTypeName): Transient $nodeType = $this->getNodeType($nodeTypeName); return TransientNode::forRegular( + NodeAggregateId::fromString('na'), + WorkspaceName::fromString('ws'), + OriginDimensionSpacePoint::fromArray([]), $nodeType, + NodeAggregateIdsByNodePaths::createForNodeType($nodeType->name, $this->nodeTypeManager), $this->nodeTypeManager, - $this->getMockBuilder(Context::class)->disableOriginalConstructor()->getMock(), + $this->getMockBuilder(ContentSubgraphInterface::class)->disableOriginalConstructor()->getMock(), [] ); } @@ -203,11 +214,13 @@ private function createFakeRegularTransientNode(string $nodeTypeName): Transient /** * Return a nodetype built from the nodeTypesFixture */ - private function getNodeType(string $nodeTypeName): ?NodeType + private function getNodeType(string $nodeTypeName): NodeType { $nodeType = $this->nodeTypeManager->getNodeType($nodeTypeName); - // no di here - ObjectAccess::setProperty($nodeType, 'nodeTypeManager', $this->nodeTypeManager, true); + if (!$nodeType) { + throw new \Exception('Unknown node type ' . $nodeTypeName); + } + return $nodeType; } } diff --git a/Tests/Unit/NodeMockTrait.php b/Tests/Unit/NodeMockTrait.php new file mode 100644 index 0000000..ceae37b --- /dev/null +++ b/Tests/Unit/NodeMockTrait.php @@ -0,0 +1,44 @@ +=7.4", - "neos/neos": "^7.3 || ^8.0", - "neos/neos-ui": "~7.3.18 || ~8.0.13 || ~8.1.10 || ~8.2.10 || ~8.3.1 || ~8.4.0" + "neos/neos": "^9.0", + "neos/neos-ui": "^9.0" }, "require-dev": { "phpstan/phpstan": "^1.10" diff --git a/phpstan.neon b/phpstan.neon index 3abf7a6..d0cf7bd 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,4 +1,4 @@ parameters: - level: 5 + level: 8 paths: - Classes