Skip to content

Commit

Permalink
Merge pull request #3519 from neos/feature/improveNodeCreationHandler…
Browse files Browse the repository at this point in the history
…Interface

!!! FEATURE: Overhaul `NodeCreationHandlerInterface`
  • Loading branch information
mhsdesign authored Mar 15, 2024
2 parents 4efe640 + 132dbd6 commit f9c9a23
Show file tree
Hide file tree
Showing 20 changed files with 1,238 additions and 375 deletions.
91 changes: 78 additions & 13 deletions Classes/Domain/Model/Changes/AbstractCreate.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,21 @@

use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint;
use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface;
use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode;
use Neos\ContentRepository\Core\NodeType\NodeTypeName;
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyOccupied;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepository\Core\SharedModel\Node\NodeName;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\ObjectManagement\ObjectManagerInterface;
use Neos\Neos\Ui\Domain\NodeCreation\NodeCreationHandlerFactoryInterface;
use Neos\Neos\Ui\Domain\Service\NodePropertyConversionService;
use Neos\Neos\Ui\Exception\InvalidNodeCreationHandlerException;
use Neos\Neos\Ui\NodeCreationHandler\NodeCreationHandlerInterface;
use Neos\Neos\Ui\Domain\NodeCreation\NodeCreationCommands;
use Neos\Neos\Ui\Domain\NodeCreation\NodeCreationElements;
use Neos\Neos\Ui\Domain\NodeCreation\NodeCreationHandlerInterface;
use Neos\Utility\PositionalArraySorter;

/**
Expand All @@ -31,6 +38,16 @@
*/
abstract class AbstractCreate extends AbstractStructuralChange
{
/**
* @Flow\Inject
*/
protected ObjectManagerInterface $objectManager;

/**
* @Flow\Inject
*/
protected NodePropertyConversionService $nodePropertyConversionService;

/**
* The type of the node that will be created
*/
Expand All @@ -48,6 +65,11 @@ abstract class AbstractCreate extends AbstractStructuralChange
*/
protected ?string $name = null;

/**
* An (optional) node aggregate identifier that will be used for testing
*/
protected ?NodeAggregateId $nodeAggregateId = null;

/**
* @param string $nodeTypeName
*/
Expand Down Expand Up @@ -88,6 +110,16 @@ public function getName(): ?string
return $this->name;
}

public function setNodeAggregateId(string $nodeAggregateId): void
{
$this->nodeAggregateId = NodeAggregateId::fromString($nodeAggregateId);
}

public function getNodeAggregateId(): ?NodeAggregateId
{
return $this->nodeAggregateId;
}

/**
* @param Node $parentNode
* @param NodeAggregateId|null $succeedingSiblingNodeAggregateId
Expand All @@ -106,7 +138,7 @@ protected function createNode(
? NodeName::fromString($this->getName())
: null;

$nodeAggregateId = NodeAggregateId::create(); // generate a new NodeAggregateId
$nodeAggregateId = $this->getNodeAggregateId() ?? NodeAggregateId::create(); // generate a new NodeAggregateId

$contentRepository = $this->contentRepositoryRegistry->get($parentNode->subgraphIdentity->contentRepositoryId);
$workspace = $this->contentRepositoryRegistry->get($this->subject->subgraphIdentity->contentRepositoryId)
Expand All @@ -127,9 +159,24 @@ protected function createNode(
$nodeName
);

$command = $this->applyNodeCreationHandlers($command, $nodeTypeName, $contentRepository);
$commands = $this->applyNodeCreationHandlers(
NodeCreationCommands::fromFirstCommand(
$command,
$contentRepository->getNodeTypeManager()
),
$this->nodePropertyConversionService->convertNodeCreationElements(
$contentRepository->getNodeTypeManager()->getNodeType($nodeTypeName),
$this->getData() ?: []
),
$nodeTypeName,
$contentRepository
);

foreach ($commands as $command) {
$contentRepository->handle($command)
->block();
}

$contentRepository->handle($command)->block();
/** @var Node $newlyCreatedNode */
$newlyCreatedNode = $this->contentRepositoryRegistry->subgraphForNode($parentNode)
->findNodeById($nodeAggregateId);
Expand All @@ -145,27 +192,45 @@ protected function createNode(
* @throws InvalidNodeCreationHandlerException
*/
protected function applyNodeCreationHandlers(
CreateNodeAggregateWithNode $command,
NodeCreationCommands $commands,
NodeCreationElements $elements,
NodeTypeName $nodeTypeName,
ContentRepository $contentRepository
): CreateNodeAggregateWithNode {
$data = $this->getData() ?: [];
): NodeCreationCommands {
$nodeType = $contentRepository->getNodeTypeManager()->getNodeType($nodeTypeName);
if (!isset($nodeType->getOptions()['nodeCreationHandlers'])
|| !is_array($nodeType->getOptions()['nodeCreationHandlers'])) {
return $command;
return $commands;
}
foreach ((new PositionalArraySorter($nodeType->getOptions()['nodeCreationHandlers']))->toArray() as $nodeCreationHandlerConfiguration) {
$nodeCreationHandler = new $nodeCreationHandlerConfiguration['nodeCreationHandler']();
foreach ((new PositionalArraySorter($nodeType->getOptions()['nodeCreationHandlers']))->toArray() as $key => $nodeCreationHandlerConfiguration) {
if (!isset($nodeCreationHandlerConfiguration['factoryClassName'])) {
throw new InvalidNodeCreationHandlerException(sprintf(
'Node creation handler "%s" has no "factoryClassName" specified.',
$key
), 1697750190);
}

$nodeCreationHandlerFactory = $this->objectManager->get($nodeCreationHandlerConfiguration['factoryClassName']);
if (!$nodeCreationHandlerFactory instanceof NodeCreationHandlerFactoryInterface) {
throw new InvalidNodeCreationHandlerException(sprintf(
'Node creation handler "%s" didnt specify factory class of type %s. Got "%s"',
$key,
NodeCreationHandlerFactoryInterface::class,
get_class($nodeCreationHandlerFactory)
), 1697750193);
}

$nodeCreationHandler = $nodeCreationHandlerFactory->build($contentRepository);
if (!$nodeCreationHandler instanceof NodeCreationHandlerInterface) {
throw new InvalidNodeCreationHandlerException(sprintf(
'Expected %s but got "%s"',
'Node creation handler "%s" didnt specify factory class of type %s. Got "%s"',
$key,
NodeCreationHandlerInterface::class,
get_class($nodeCreationHandler)
), 1364759956);
}
$command = $nodeCreationHandler->handle($command, $data, $contentRepository);
$commands = $nodeCreationHandler->handle($commands, $elements);
}
return $command;
return $commands;
}
}
3 changes: 1 addition & 2 deletions Classes/Domain/Model/Changes/Property.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,7 @@ public function apply(): void
);
} else {
$value = $this->nodePropertyConversionService->convert(
$this->getNodeType($subject),
$propertyName,
$this->getNodeType($subject)->getPropertyType($propertyName),
$this->getValue()
);

Expand Down
124 changes: 124 additions & 0 deletions Classes/Domain/NodeCreation/NodeCreationCommands.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Neos\Neos\Ui\Domain\NodeCreation;

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

use Neos\ContentRepository\Core\CommandHandler\CommandInterface;
use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode;
use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths;
use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate;
use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\EnableNodeAggregate;
use Neos\ContentRepository\Core\Feature\NodeDuplication\Command\CopyNodesRecursively;
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\NodeType\NodeTypeManager;

/**
* A collection of commands that describe a node creation from the Neos Ui.
*
* The node creation can be enriched via a node creation handler {@see NodeCreationHandlerInterface}
*
* The first command points to the triggered node creation command.
* To not contradict the users intend it is ensured that the initial node
* creation will be mostly preserved by only allowing to add additional properties.
*
* Additional commands can be also appended, to be run after the initial node creation command.
* All commands will be executed blocking.
*
* You can retrieve the subgraph or the parent node (where the first node will be created in) the following way:
*
* $subgraph = $contentRepository->getContentGraph()->getSubgraph(
* $commands->first->contentStreamId,
* $commands->first->originDimensionSpacePoint->toDimensionSpacePoint(),
* VisibilityConstraints::frontend()
* );
* $parentNode = $subgraph->findNodeById($commands->first->parentNodeAggregateId);
*
* @implements \IteratorAggregate<int, CommandInterface>
* @internal Especially the constructors
*/
final readonly class NodeCreationCommands implements \IteratorAggregate
{
/**
* The initial node creation command.
* It is only allowed to change its properties via {@see self::withInitialPropertyValues()}
*/
public CreateNodeAggregateWithNode $first;

/**
* Add a list of commands that are executed after the initial created command was run.
* This allows to create child-nodes and append other allowed commands.
*
* @var array<int,CreateNodeAggregateWithNode|SetNodeProperties|DisableNodeAggregate|EnableNodeAggregate|SetNodeReferences|CopyNodesRecursively>
*/
public array $additionalCommands;

private function __construct(
CreateNodeAggregateWithNode $first,
CreateNodeAggregateWithNode|SetNodeProperties|DisableNodeAggregate|EnableNodeAggregate|SetNodeReferences|CopyNodesRecursively ...$additionalCommands
) {
$this->first = $first;
$this->additionalCommands = array_values($additionalCommands);
}

/**
* @internal to guarantee that the initial create command is mostly preserved as intended.
* You can use {@see self::withInitialPropertyValues()} to add new properties of the to be created node.
*/
public static function fromFirstCommand(
CreateNodeAggregateWithNode $first,
NodeTypeManager $nodeTypeManager
): self {
$tetheredDescendantNodeAggregateIds = NodeAggregateIdsByNodePaths::createForNodeType(
$first->nodeTypeName,
$nodeTypeManager
);
return new self(
$first->withTetheredDescendantNodeAggregateIds($tetheredDescendantNodeAggregateIds),
);
}

/**
* Augment the first {@see CreateNodeAggregateWithNode} command with altered properties.
*
* The properties will be completely replaced.
* To merge the properties please use:
*
* $commands->withInitialPropertyValues(
* $commands->first->initialPropertyValues
* ->withValue('album', 'rep')
* )
*
*/
public function withInitialPropertyValues(PropertyValuesToWrite $newInitialPropertyValues): self
{
return new self(
$this->first->withInitialPropertyValues($newInitialPropertyValues),
...$this->additionalCommands
);
}

public function withAdditionalCommands(
CreateNodeAggregateWithNode|SetNodeProperties|DisableNodeAggregate|EnableNodeAggregate|SetNodeReferences|CopyNodesRecursively ...$additionalCommands
): self {
return new self($this->first, ...$this->additionalCommands, ...$additionalCommands);
}

/**
* @return \Traversable<int, CommandInterface>
*/
public function getIterator(): \Traversable
{
yield from [$this->first, ...$this->additionalCommands];
}
}
96 changes: 96 additions & 0 deletions Classes/Domain/NodeCreation/NodeCreationElements.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

declare(strict_types=1);

namespace Neos\Neos\Ui\Domain\NodeCreation;

use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds;

/**
* Holds the deserialized elements of the submitted node creation dialog form
*
* Elements are configured like properties or references in the schema,
* but its up to the node-creation-handler if they are handled in any way or just left out.
*
* Elements that are of simple types or objects, will be available according to its type.
* For example myImage will be an actual image object instance.
*
* Vendor.Site:Content:
* ui:
* creationDialog:
* elements:
* myString:
* type: string
* myImage:
* type: Neos\Media\Domain\Model\ImageInterface
*
* Elements that refer to nodes are of type `references` or `reference`.
* They will be available as {@see NodeAggregateIds} collection.
*
* Vendor.Site:Content:
* ui:
* creationDialog:
* elements:
* myReferences:
* type: references
*
* The naming `references` in the `element` configuration does not refer to the content repository reference edges.
* Referring to a node will just denote that an editor will be used capable of returning node ids.
* The node ids might be used for setting references but that is up to a node-creation-handler.
*
* To promoted properties / references the same rules apply:
*
* Vendor.Site:Content:
* properties:
* myString:
* type: string
* ui:
* showInCreationDialog: true
*
* @implements \IteratorAggregate<string, mixed>
* @internal Especially the constructor and the serialized data
*/
final readonly class NodeCreationElements implements \IteratorAggregate
{
/**
* @param array<string, mixed> $elementValues
* @param array<int|string, mixed> $serializedValues
* @internal you should not need to construct this
*/
public function __construct(
private array $elementValues,
private array $serializedValues,
) {
}

public function has(string $name): bool
{
return isset($this->elementValues[$name]);
}

/**
* Returns the type according to the element schema
* For elements that refer to a node {@see NodeAggregateIds} will be returned.
*/
public function get(string $name): mixed
{
return $this->elementValues[$name] ?? null;
}

/**
* @internal returns values formatted by the internal format used for the Ui
* @return \Traversable<int|string, mixed>
*/
public function serialized(): \Traversable
{
yield from $this->serializedValues;
}

/**
* @return \Traversable<string,mixed>
*/
public function getIterator(): \Traversable
{
yield from $this->elementValues;
}
}
Loading

0 comments on commit f9c9a23

Please sign in to comment.