Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

!!! FEATURE: Overhaul NodeCreationHandlerInterface #3519

Merged
merged 32 commits into from
Mar 15, 2024
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c3dc494
FEATURE: Allow `NodeCreationHandlerInterface` to append additional co…
mhsdesign Jun 11, 2023
e7a7991
TASK: Minor refinements
mhsdesign Jun 24, 2023
7b751a1
TASK: Possibility to make nodeId deterministic (for testing)
mhsdesign Jul 23, 2023
ac5c5fb
TASK: NodeCreationCommands: Guarantee that the $tetheredDescendantNod…
mhsdesign Oct 18, 2023
258b43c
TASK: Use factory for `NodeCreationHandlerInterface` and make it impl…
mhsdesign Oct 20, 2023
a9e248c
TASK: Fix `yield from` problem
mhsdesign Feb 17, 2024
67b533d
TASK: Use anonymous class pattern instead of factory + class
mhsdesign Feb 17, 2024
a308ab7
TASK: Replace testing `ImagePropertyNodeCreationHandler` with `showIn…
mhsdesign Feb 17, 2024
5d26ea2
FEATURE: Introduce `NodeCreationElements` abstraction do encapsulate …
mhsdesign Feb 17, 2024
2e6a225
TASK: Remove obsolete `contentTitle` nodeCreationHandler
mhsdesign Feb 17, 2024
fa797c9
TASK: Rename `DocumentTitleNodeCreationHandler`
mhsdesign Feb 17, 2024
995c762
TASK: Rename `CreationDialogPropertiesCreationHandler`
mhsdesign Feb 17, 2024
0b8cfb4
TASK: Move NodeCreation stuff around
mhsdesign Feb 17, 2024
f9fbf3f
BUGFIX: Reference support in NodeCreationHandler
mhsdesign Feb 17, 2024
ee15b78
TASK: Improve documentation
mhsdesign Feb 17, 2024
9890e05
TASK: Move `NodeCreationHandler` implementations to fitting sub names…
mhsdesign Feb 24, 2024
96454ff
TASK: Simply `NodeCreationElements` by not differentiating between pr…
mhsdesign Feb 24, 2024
9847009
WIP: Add temporary patch for testing
mhsdesign Feb 24, 2024
5659624
TASK: Move `CreationDialogPostprocessor` to Neos.Ui
mhsdesign Feb 28, 2024
d2db352
TASK: Rename `CreationDialogPostprocessor` to `CreationDialogNodeType…
mhsdesign Feb 28, 2024
366ec92
TASK: Prepare `CreationDialogPostprocessor` to be more generic
mhsdesign Mar 1, 2024
6f7db58
TASK: Move `creationDialog` logic of `DefaultPropertyEditorPostproces…
mhsdesign Mar 1, 2024
ea20dc1
BUGFIX: 3509 Unify document title label and node creation title label
mhsdesign Mar 1, 2024
563d830
BUGFIX: Make sure that the title appears first in the creation dialog
mhsdesign Mar 2, 2024
860e9d1
TASK: Remove obsolete `title` property set operation in `UriPathSegme…
mhsdesign Mar 2, 2024
eec2840
BUGFIX: Avoid toooo random uri-paths segments like `65d0ba5d8f4593-93…
mhsdesign Mar 2, 2024
9b7c3b1
TASK: Dont leak `ContentRepositoryServiceFactoryDependencies` into th…
mhsdesign Mar 12, 2024
fd71037
Merge remote-tracking branch 'origin/9.0' into feature/improveNodeCre…
mhsdesign Mar 12, 2024
4e12c87
TASK: Make nodecreation handler internal
mhsdesign Mar 13, 2024
d4c0bf5
TASK: Simplify `getIterator`
mhsdesign Mar 13, 2024
554879b
Merge remote-tracking branch 'origin/9.0' into feature/improveNodeCre…
mhsdesign Mar 14, 2024
132dbd6
TASK: Adjust to workspace aware commands
mhsdesign Mar 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 78 additions & 13 deletions Classes/Domain/Model/Changes/AbstractCreate.php
Original file line number Diff line number Diff line change
@@ -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;

/**
@@ -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
*/
@@ -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;
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved

/**
* @param string $nodeTypeName
*/
@@ -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;
}
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved

/**
* @param Node $parentNode
* @param NodeAggregateId|null $succeedingSiblingNodeAggregateId
@@ -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)
@@ -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);
@@ -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
@@ -195,8 +195,7 @@ public function apply(): void
);
} else {
$value = $this->nodePropertyConversionService->convert(
$this->getNodeType($subject),
$propertyName,
$this->getNodeType($subject)->getPropertyType($propertyName),
$this->getValue()
);

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
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be great if NodeCreationElements would implement ArrayAccess and return ValueAccessors (as introduced in neos/flow-development-collection#3149). This would allow consumers to write:

$myField = $elements['myField']->stringOrNull();

Otherwise, implementers would have to validate this stuff on their own (and likely in a very similar fashion).

But this is just a nice-to-have 😇

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmmm i somehow still find the naming and handling of the ValueAccessors a little strange and mostly like to validate, or gracefully validate (string cast or no error) which this cannot, on my own.

I think we should put the ValueAccessors first under a real stress test where we heavily read configuration (like the content repository registry, or the site configuration), so that we can be sure that it is now final api and feature complete ;)

But lets discuss this in person as well ;)

{
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;
}
grebaldi marked this conversation as resolved.
Show resolved Hide resolved

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

Unchanged files with check annotations Beta

const {q} = backend.get();
const [fullyLoadedFocusedNode] = yield q(focusedNode).get();
if (fullyLoadedFocusedNode) {

Check warning on line 48 in packages/neos-ui-sagas/src/UI/Inspector/index.js

GitHub Actions / Code style

Blocks are nested too deeply (5). Maximum allowed is 4
yield put(actions.CR.Nodes.merge({
[fullyLoadedFocusedNode.contextPath]: fullyLoadedFocusedNode
}));