diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphAdapter.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphAdapter.php new file mode 100644 index 00000000000..f8095ebf72d --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphAdapter.php @@ -0,0 +1,507 @@ +workspaceName === null && $this->contentStreamId === null) { + throw new \InvalidArgumentException('Neither ContentStreamId nor WorkspaceName given in creation of ContentGraphAdapter, one is required.', 1712746528); + } + + $this->nodeQueryBuilder = new NodeQueryBuilder($dbalConnection, $tableNamePrefix); + } + + public function rootNodeAggregateWithTypeExists(NodeTypeName $nodeTypeName): bool + { + try { + return (bool)$this->findRootNodeAggregateByType($nodeTypeName); + } catch (\Exception $_) { + } + + return false; + } + + public function findRootNodeAggregateByType(NodeTypeName $nodeTypeName): ?NodeAggregate + { + $rootNodeAggregateQueryBuilder = $this->nodeQueryBuilder->buildFindRootNodeAggregatesQuery($this->getContentStreamId(), FindRootNodeAggregatesFilter::create(nodeTypeName: $nodeTypeName)); + $rootNodeAggregates = NodeAggregates::fromArray(iterator_to_array($this->mapQueryBuilderToNodeAggregates($rootNodeAggregateQueryBuilder))); + if ($rootNodeAggregates->count() < 1) { + throw RootNodeAggregateDoesNotExist::butWasExpectedTo($nodeTypeName); + } + if ($rootNodeAggregates->count() > 1) { + $ids = []; + foreach ($rootNodeAggregates as $rootNodeAggregate) { + $ids[] = $rootNodeAggregate->nodeAggregateId->value; + } + throw new \RuntimeException(sprintf( + 'More than one root node aggregate of type "%s" found (IDs: %s).', + $nodeTypeName->value, + implode(', ', $ids) + )); + } + + return $rootNodeAggregates->first(); + } + + public function findParentNodeAggregates(NodeAggregateId $childNodeAggregateId): iterable + { + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeAggregateQuery() + ->innerJoin('n', $this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'ch', 'ch.parentnodeanchor = n.relationanchorpoint') + ->innerJoin('ch', $this->nodeQueryBuilder->contentGraphTableNames->node(), 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') + ->andWhere('ch.contentstreamid = :contentStreamId') + ->andWhere('cn.nodeaggregateid = :nodeAggregateId') + ->setParameters([ + 'nodeAggregateId' => $childNodeAggregateId->value, + 'contentStreamId' => $this->getContentStreamId()->value + ]); + + return $this->mapQueryBuilderToNodeAggregates($queryBuilder); + } + + public function findNodeAggregateById(NodeAggregateId $nodeAggregateId): ?NodeAggregate + { + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeAggregateQuery() + ->andWhere('n.nodeaggregateid = :nodeAggregateId') + ->orderBy('n.relationanchorpoint', 'DESC') + ->setParameters([ + 'nodeAggregateId' => $nodeAggregateId->value, + 'contentStreamId' => $this->getContentStreamId()->value + ]); + + return $this->nodeFactory->mapNodeRowsToNodeAggregate( + $this->fetchRows($queryBuilder), + $this->getContentStreamId(), + VisibilityConstraints::withoutRestrictions() + ); + } + + public function findParentNodeAggregateByChildOriginDimensionSpacePoint(NodeAggregateId $childNodeAggregateId, OriginDimensionSpacePoint $childOriginDimensionSpacePoint): ?NodeAggregate + { + $subQueryBuilder = $this->dbalConnection->createQueryBuilder() + ->select('pn.nodeaggregateid') + ->from($this->nodeQueryBuilder->contentGraphTableNames->node(), 'pn') + ->innerJoin('pn', $this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('ch', $this->nodeQueryBuilder->contentGraphTableNames->node(), 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') + ->where('ch.contentstreamid = :contentStreamId') + ->andWhere('ch.dimensionspacepointhash = :childOriginDimensionSpacePointHash') + ->andWhere('cn.nodeaggregateid = :childNodeAggregateId') + ->andWhere('cn.origindimensionspacepointhash = :childOriginDimensionSpacePointHash'); + + $queryBuilder = $this->dbalConnection->createQueryBuilder() + ->select('n.*, h.name, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->from($this->nodeQueryBuilder->contentGraphTableNames->node(), 'n') + ->innerJoin('n', $this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') + ->innerJoin('h', $this->nodeQueryBuilder->contentGraphTableNames->dimensionSpacePoints(), 'dsp', 'dsp.hash = h.dimensionspacepointhash') + ->where('n.nodeaggregateid = (' . $subQueryBuilder->getSQL() . ')') + ->andWhere('h.contentstreamid = :contentStreamId') + ->setParameters([ + 'contentStreamId' => $this->getContentStreamId()->value, + 'childNodeAggregateId' => $childNodeAggregateId->value, + 'childOriginDimensionSpacePointHash' => $childOriginDimensionSpacePoint->hash, + ]); + + return $this->nodeFactory->mapNodeRowsToNodeAggregate( + $this->fetchRows($queryBuilder), + $this->getContentStreamId(), + VisibilityConstraints::withoutRestrictions() + ); + } + + public function findChildNodeAggregates(NodeAggregateId $parentNodeAggregateId): iterable + { + $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->getContentStreamId()); + + return $this->mapQueryBuilderToNodeAggregates($queryBuilder); + } + + public function findTetheredChildNodeAggregates(NodeAggregateId $parentNodeAggregateId): iterable + { + $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->getContentStreamId()) + ->andWhere('cn.classification = :tetheredClassification') + ->setParameter('tetheredClassification', NodeAggregateClassification::CLASSIFICATION_TETHERED->value); + + return $this->mapQueryBuilderToNodeAggregates($queryBuilder); + } + + public function getDimensionSpacePointsOccupiedByChildNodeName(NodeName $nodeName, NodeAggregateId $parentNodeAggregateId, OriginDimensionSpacePoint $parentNodeOriginDimensionSpacePoint, DimensionSpacePointSet $dimensionSpacePointsToCheck): DimensionSpacePointSet + { + $queryBuilder = $this->createQueryBuilder() + ->select('dsp.dimensionspacepoint, h.dimensionspacepointhash') + ->from($this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'h') + ->innerJoin('h', $this->nodeQueryBuilder->contentGraphTableNames->node(), 'n', 'n.relationanchorpoint = h.parentnodeanchor') + ->innerJoin('h', $this->nodeQueryBuilder->contentGraphTableNames->dimensionSpacePoints(), 'dsp', 'dsp.hash = h.dimensionspacepointhash') + ->innerJoin('n', $this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'ph', 'ph.childnodeanchor = n.relationanchorpoint') + ->where('n.nodeaggregateid = :parentNodeAggregateId') + ->andWhere('n.origindimensionspacepointhash = :parentNodeOriginDimensionSpacePointHash') + ->andWhere('ph.contentstreamid = :contentStreamId') + ->andWhere('h.contentstreamid = :contentStreamId') + ->andWhere('h.dimensionspacepointhash IN (:dimensionSpacePointHashes)') + ->andWhere('h.name = :nodeName') + ->setParameters([ + 'parentNodeAggregateId' => $parentNodeAggregateId->value, + 'parentNodeOriginDimensionSpacePointHash' => $parentNodeOriginDimensionSpacePoint->hash, + 'contentStreamId' => $this->getContentStreamId()->value, + 'dimensionSpacePointHashes' => $dimensionSpacePointsToCheck->getPointHashes(), + 'nodeName' => $nodeName->value + ], [ + 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY, + ]); + $dimensionSpacePoints = []; + foreach ($this->fetchRows($queryBuilder) as $hierarchyRelationData) { + $dimensionSpacePoints[$hierarchyRelationData['dimensionspacepointhash']] = DimensionSpacePoint::fromJsonString($hierarchyRelationData['dimensionspacepoint']); + } + + return new DimensionSpacePointSet($dimensionSpacePoints); + } + + public function findChildNodeAggregatesByName(NodeAggregateId $parentNodeAggregateId, NodeName $name): iterable + { + $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->getContentStreamId()) + ->andWhere('ch.name = :relationName') + ->setParameter('relationName', $name->value); + + return $this->mapQueryBuilderToNodeAggregates($queryBuilder); + } + + public function subgraphContainsNodes(DimensionSpacePoint $dimensionSpacePoint): bool + { + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->getContentStreamId(), $dimensionSpacePoint, 'n', 'COUNT(*)'); + try { + $result = $this->executeQuery($queryBuilder)->fetchOne(); + if (!is_int($result)) { + throw new \RuntimeException(sprintf('Expected result to be of type integer but got: %s', get_debug_type($result)), 1678366902); + } + + return $result > 0; + } catch (DbalDriverException | DbalException $e) { + throw new \RuntimeException(sprintf('Failed to count all nodes: %s', $e->getMessage()), 1678364741, $e); + } + } + + public function findNodeInSubgraph(DimensionSpacePoint $coveredDimensionSpacePoint, NodeAggregateId $nodeAggregateId): ?Node + { + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeByIdQuery($nodeAggregateId, $this->getContentStreamId(), $coveredDimensionSpacePoint); + + // TODO: Do we need subtree tag support here, not for visibility at least + return $this->fetchNode($queryBuilder, $coveredDimensionSpacePoint); + } + + public function findChildNodesInSubgraph(DimensionSpacePoint $coveredDimensionSpacePoint, NodeAggregateId $parentNodeAggregateId): Nodes + { + $queryBuilder = $this->buildChildNodesQuery($parentNodeAggregateId, $coveredDimensionSpacePoint); + $queryBuilder->addOrderBy('h.position'); + + return $this->fetchNodes($queryBuilder, $coveredDimensionSpacePoint); + } + + public function findParentNodeInSubgraph(DimensionSpacePoint $coveredDimensionSpacePoint, NodeAggregateId $childNodeAggregateId): ?Node + { + $queryBuilder = $this->nodeQueryBuilder->buildBasicParentNodeQuery($childNodeAggregateId, $this->getContentStreamId(), $coveredDimensionSpacePoint); + return $this->fetchNode($queryBuilder, $coveredDimensionSpacePoint); + } + + public function findChildNodeByNameInSubgraph(DimensionSpacePoint $coveredDimensionSpacePoint, NodeAggregateId $parentNodeAggregateId, NodeName $nodeName): ?Node + { + $startingNode = $this->findNodeInSubgraph($coveredDimensionSpacePoint, $parentNodeAggregateId); + + return $startingNode + ? $this->findNodeByPathFromStartingNode(NodePath::fromNodeNames($nodeName), $startingNode, $coveredDimensionSpacePoint) + : null; + } + + public function findPreceedingSiblingNodesInSubgraph(DimensionSpacePoint $coveredDimensionSpacePoint, NodeAggregateId $startingSiblingNodeAggregateId): Nodes + { + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeSiblingsQuery(true, $startingSiblingNodeAggregateId, $this->getContentStreamId(), $coveredDimensionSpacePoint); + + return $this->fetchNodes($queryBuilder, $coveredDimensionSpacePoint); + } + + public function findSucceedingSiblingNodesInSubgraph(DimensionSpacePoint $coveredDimensionSpacePoint, NodeAggregateId $startingSiblingNodeAggregateId): Nodes + { + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeSiblingsQuery(false, $startingSiblingNodeAggregateId, $this->getContentStreamId(), $coveredDimensionSpacePoint); + + return $this->fetchNodes($queryBuilder, $coveredDimensionSpacePoint); + } + + public function hasContentStream(): bool + { + try { + /* @var $state string|false */ + $state = $this->dbalConnection->executeQuery( + 'SELECT state FROM cr_default_p_contentstream WHERE contentStreamId = :contentStreamId', + [ + 'contentStreamId' => $this->getContentStreamId()->value, + ] + )->fetchOne(); + + return $state !== false; + } catch (ContentStreamDoesNotExistYet $_) { + return false; + } + } + + public function findStateForContentStream(): ?ContentStreamState + { + /* @var $state string|false */ + $state = $this->dbalConnection->executeQuery( + 'SELECT state FROM cr_default_p_contentstream WHERE contentStreamId = :contentStreamId', + [ + 'contentStreamId' => $this->getContentStreamId()->value, + ] + )->fetchOne(); + + return ContentStreamState::tryFrom($state ?: ''); + } + + public function findVersionForContentStream(): MaybeVersion + { + /* @var $version int|false */ + $version = $this->dbalConnection->executeQuery( + 'SELECT version FROM cr_default_p_contentstream WHERE contentStreamId = :contentStreamId', + [ + 'contentStreamId' => $this->getContentStreamId()->value, + ] + )->fetchOne(); + + $versionObject = $version !== false ? Version::fromInteger($version) : null; + return MaybeVersion::fromVersionOrNull($versionObject); + } + + /** + * @param QueryBuilder $queryBuilder + * @return iterable + */ + private function mapQueryBuilderToNodeAggregates(QueryBuilder $queryBuilder): iterable + { + return $this->nodeFactory->mapNodeRowsToNodeAggregates( + $this->fetchRows($queryBuilder), + $this->getContentStreamId(), + VisibilityConstraints::withoutRestrictions() + ); + } + + private function createQueryBuilder(): QueryBuilder + { + return $this->dbalConnection->createQueryBuilder(); + } + + private function buildChildNodesQuery(NodeAggregateId $parentNodeAggregateId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder + { + $queryBuilder = $this->nodeQueryBuilder->buildBasicChildNodesQuery($parentNodeAggregateId, $this->getContentStreamId(), $dimensionSpacePoint); + + return $queryBuilder; + } + + /** + * @return array> + */ + private function fetchRows(QueryBuilder $queryBuilder): array + { + $result = $queryBuilder->execute(); + if (!$result instanceof Result) { + throw new \RuntimeException(sprintf('Failed to execute query. Expected result to be of type %s, got: %s', Result::class, get_debug_type($result)), 1701443535); + } + try { + return $result->fetchAllAssociative(); + } catch (DriverException | DBALException $e) { + throw new \RuntimeException(sprintf('Failed to fetch rows from database: %s', $e->getMessage()), 1701444358, $e); + } + } + + /** + * @param QueryBuilder $queryBuilder + * @return \Doctrine\DBAL\ForwardCompatibility\Result + * @throws DbalException + */ + private function executeQuery(QueryBuilder $queryBuilder): Result + { + $result = $queryBuilder->execute(); + if (!$result instanceof Result) { + throw new \RuntimeException(sprintf('Expected instance of %s, got %s', Result::class, get_debug_type($result)), 1678370012); + } + + return $result; + } + + private function findNodeByPathFromStartingNode(NodePath $path, Node $startingNode, DimensionSpacePoint $dimensionSpacePoint): ?Node + { + $currentNode = $startingNode; + + foreach ($path->getParts() as $edgeName) { + $currentNode = $this->findChildNodeConnectedThroughEdgeName($currentNode->nodeAggregateId, $edgeName, $dimensionSpacePoint); + if ($currentNode === null) { + return null; + } + } + + return $currentNode; + } + + /** + * Find a single child node by its name + * + * @return Node|null the node that is connected to its parent with the specified $nodeName, or NULL if no matching node exists or the parent node is not accessible + */ + private function findChildNodeConnectedThroughEdgeName(NodeAggregateId $parentNodeAggregateId, NodeName $nodeName, DimensionSpacePoint $dimensionSpacePoint): ?Node + { + $queryBuilder = $this->createQueryBuilder() + ->select('cn.*, h.name, h.subtreetags') + ->from($this->nodeQueryBuilder->contentGraphTableNames->node(), 'pn') + ->innerJoin('pn', $this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'h', 'h.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('pn', $this->nodeQueryBuilder->contentGraphTableNames->node(), 'cn', 'cn.relationanchorpoint = h.childnodeanchor') + ->where('pn.nodeaggregateid = :parentNodeAggregateId')->setParameter('parentNodeAggregateId', $parentNodeAggregateId->value) + ->andWhere('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->getContentStreamId()->value) + ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash) + ->andWhere('h.name = :edgeName')->setParameter('edgeName', $nodeName->value); + + return $this->fetchNode($queryBuilder, $dimensionSpacePoint); + } + + private function fetchNode(QueryBuilder $queryBuilder, DimensionSpacePoint $dimensionSpacePoint): ?Node + { + try { + $nodeRow = $this->executeQuery($queryBuilder)->fetchAssociative(); + } catch (DbalDriverException | DbalException $e) { + throw new \RuntimeException(sprintf('Failed to fetch node: %s', $e->getMessage()), 1678286030, $e); + } + if ($nodeRow === false) { + return null; + } + + return $this->nodeFactory->mapNodeRowToNode( + $nodeRow, + $this->getContentStreamId(), + $dimensionSpacePoint, + VisibilityConstraints::withoutRestrictions() + ); + } + + private function fetchNodes(QueryBuilder $queryBuilder, DimensionSpacePoint $dimensionSpacePoint): Nodes + { + try { + $nodeRows = $this->executeQuery($queryBuilder)->fetchAllAssociative(); + } catch (DbalDriverException | DbalException $e) { + throw new \RuntimeException(sprintf('Failed to fetch nodes: %s', $e->getMessage()), 1678292896, $e); + } + + return $this->nodeFactory->mapNodeRowsToNodes($nodeRows, $this->getContentStreamId(), $dimensionSpacePoint, VisibilityConstraints::withoutRestrictions()); + } + + public function getWorkspaceName(): WorkspaceName + { + if ($this->workspaceName !== null) { + return $this->workspaceName; + } + + // TODO: This table is not allowed here... + $query = $this->dbalConnection->prepare('SELECT workspacename FROM cr_default_p_workspace WHERE currentcontentstreamid LIKE :contentStreamId'); + $result = $query->executeQuery(['contentStreamId' => $this->getContentStreamId()->value]); + $workspaceNameString = $result->fetchOne(); + if (!$workspaceNameString) { + throw new WorkspaceDoesNotExist(sprintf('A workspace for the ContentStreamId "%s" was not found, cannot proceed.', $this->getContentStreamId()->value), 1712746408); + } + + $this->workspaceName = WorkspaceName::fromString($workspaceNameString); + + return $this->workspaceName; + } + + public function getContentStreamId(): ContentStreamId + { + if ($this->contentStreamId !== null) { + return $this->contentStreamId; + } + + // TODO: This table is not allowed here... + $query = $this->dbalConnection->prepare('SELECT currentcontentstreamid FROM cr_default_p_workspace WHERE workspacename LIKE :workspaceName'); + $result = $query->executeQuery(['workspaceName' => $this->getWorkspaceName()->value]); + $contentStreamIdString = $result->fetchOne(); + if (!$contentStreamIdString) { + throw new ContentStreamDoesNotExistYet(sprintf('A ContentStream for the WorkspaceName "%s" was not found, cannot proceed.', $this->workspaceName?->value), 1712750421); + } + + $this->contentStreamId = ContentStreamId::fromString($contentStreamIdString); + + return $this->contentStreamId; + } + + public function getWorkspace(): Workspace + { + $query = $this->dbalConnection->prepare('SELECT * FROM cr_default_p_workspace WHERE workspacename LIKE :workspaceName'); + $result = $query->executeQuery(['workspaceName' => $this->getWorkspaceName()->value]); + $row = $result->fetchAssociative(); + + // We can assume that we get a row otherwise getWorkspaceName would have thrown already + + return new Workspace( + /** @phpstan-ignore-next-line */ + WorkspaceName::fromString($row['workspacename']), + !empty($row['baseworkspacename']) ? WorkspaceName::fromString($row['baseworkspacename']) : null, + /** @phpstan-ignore-next-line */ + WorkspaceTitle::fromString($row['workspacetitle']), + /** @phpstan-ignore-next-line */ + WorkspaceDescription::fromString($row['workspacedescription']), + /** @phpstan-ignore-next-line */ + ContentStreamId::fromString($row['currentcontentstreamid']), + /** @phpstan-ignore-next-line */ + WorkspaceStatus::from($row['status']), + /** @phpstan-ignore-next-line */ + $row['workspaceowner'] + ); + } +} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphAdapterFactory.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphAdapterFactory.php new file mode 100644 index 00000000000..4cdc7575013 --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphAdapterFactory.php @@ -0,0 +1,60 @@ +tableNamePrefix = DoctrineDbalContentGraphProjectionFactory::graphProjectionTableNamePrefix( + $contentRepositoryId + ); + + $dimensionSpacePointsRepository = new DimensionSpacePointsRepository($this->dbalConnection, $this->tableNamePrefix); + $this->nodeFactory = new NodeFactory( + $contentRepositoryId, + $nodeTypeManager, + $propertyConverter, + $dimensionSpacePointsRepository + ); + } + + public function create(WorkspaceName $workspaceName, ContentStreamId $contentStreamId): ContentGraphAdapterInterface + { + return new ContentGraphAdapter($this->dbalConnection, $this->tableNamePrefix, $this->contentRepositoryId, $this->nodeFactory, $workspaceName, $contentStreamId); + } + + public function createFromContentStreamId(ContentStreamId $contentStreamId): ContentGraphAdapterInterface + { + return new ContentGraphAdapter($this->dbalConnection, $this->tableNamePrefix, $this->contentRepositoryId, $this->nodeFactory, null, $contentStreamId); + } + + public function createFromWorkspaceName(WorkspaceName $workspaceName): ContentGraphAdapterInterface + { + return new ContentGraphAdapter($this->dbalConnection, $this->tableNamePrefix, $this->contentRepositoryId, $this->nodeFactory, $workspaceName, null); + } +} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphAdapterFactoryBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphAdapterFactoryBuilder.php new file mode 100644 index 00000000000..a5e983cbda3 --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphAdapterFactoryBuilder.php @@ -0,0 +1,26 @@ +dbalClient->getConnection(), $contentRepositoryId, $nodeTypeManager, $propertyConverter); + } +} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphTableNames.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphTableNames.php new file mode 100644 index 00000000000..20a9cc3d062 --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphTableNames.php @@ -0,0 +1,43 @@ +tableNamePrefix . '_node'; + } + + public function hierachyRelation(): string + { + return $this->tableNamePrefix . '_hierarchyrelation'; + } + + public function dimensionSpacePoints(): string + { + return $this->tableNamePrefix . '_dimensionspacepoints'; + } + + public function referenceRelation(): string + { + return $this->tableNamePrefix . '_referencerelation'; + } + + public function checkpoint(): string + { + return $this->tableNamePrefix . '_checkpoint'; + } +} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index a3a92150aa8..cf1e223012e 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -85,6 +85,8 @@ final class DoctrineDbalContentGraphProjection implements ProjectionInterface, W private DbalCheckpointStorage $checkpointStorage; + private ContentGraphTableNames $contentGraphTableNames; + public function __construct( private readonly DbalClientInterface $dbalClient, private readonly NodeFactory $nodeFactory, @@ -94,9 +96,10 @@ public function __construct( private readonly string $tableNamePrefix, private readonly DimensionSpacePointsRepository $dimensionSpacePointsRepository ) { + $this->contentGraphTableNames = ContentGraphTableNames::withPrefix($tableNamePrefix); $this->checkpointStorage = new DbalCheckpointStorage( $this->dbalClient->getConnection(), - $this->tableNamePrefix . '_checkpoint', + $this->contentGraphTableNames->checkpoint(), self::class ); } @@ -174,10 +177,10 @@ public function reset(): void private function truncateDatabaseTables(): void { $connection = $this->dbalClient->getConnection(); - $connection->executeQuery('TRUNCATE table ' . $this->tableNamePrefix . '_node'); - $connection->executeQuery('TRUNCATE table ' . $this->tableNamePrefix . '_hierarchyrelation'); - $connection->executeQuery('TRUNCATE table ' . $this->tableNamePrefix . '_referencerelation'); - $connection->executeQuery('TRUNCATE table ' . $this->tableNamePrefix . '_dimensionspacepoints'); + $connection->executeQuery('TRUNCATE table ' . $this->contentGraphTableNames->node()); + $connection->executeQuery('TRUNCATE table ' . $this->contentGraphTableNames->hierachyRelation()); + $connection->executeQuery('TRUNCATE table ' . $this->contentGraphTableNames->referenceRelation()); + $connection->executeQuery('TRUNCATE table ' . $this->contentGraphTableNames->dimensionSpacePoints()); } public function canHandle(EventInterface $event): bool @@ -311,7 +314,7 @@ private function whenRootNodeAggregateDimensionsWereUpdated(RootNodeAggregateDim $this->transactional(function () use ($rootNodeAnchorPoint, $event) { // delete all hierarchy edges of the root node $this->getDatabaseConnection()->executeUpdate(' - DELETE FROM ' . $this->tableNamePrefix . '_hierarchyrelation + DELETE FROM ' . $this->contentGraphTableNames->hierachyRelation() . ' WHERE parentnodeanchor = :parentNodeAnchor AND childnodeanchor = :childNodeAnchor @@ -360,8 +363,8 @@ private function whenNodeAggregateNameWasChanged(NodeAggregateNameWasChanged $ev { $this->transactional(function () use ($event, $eventEnvelope) { $this->getDatabaseConnection()->executeStatement(' - UPDATE ' . $this->tableNamePrefix . '_hierarchyrelation h - INNER JOIN ' . $this->tableNamePrefix . '_node n on + UPDATE ' . $this->contentGraphTableNames->hierachyRelation() . ' h + INNER JOIN ' . $this->contentGraphTableNames->node() . ' n on h.childnodeanchor = n.relationanchorpoint SET h.name = :newName, @@ -597,7 +600,7 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void // 1) Copy HIERARCHY RELATIONS (this is the MAIN OPERATION here) // $this->getDatabaseConnection()->executeUpdate(' - INSERT INTO ' . $this->tableNamePrefix . '_hierarchyrelation ( + INSERT INTO ' . $this->contentGraphTableNames->hierachyRelation() . ' ( parentnodeanchor, childnodeanchor, `name`, @@ -615,7 +618,7 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void h.subtreetags, "' . $event->newContentStreamId->value . '" AS contentstreamid FROM - ' . $this->tableNamePrefix . '_hierarchyrelation h + ' . $this->contentGraphTableNames->hierachyRelation() . ' h WHERE h.contentstreamid = :sourceContentStreamId ', [ 'sourceContentStreamId' => $event->sourceContentStreamId->value @@ -632,7 +635,7 @@ private function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): vo // Drop hierarchy relations $this->getDatabaseConnection()->executeUpdate(' - DELETE FROM ' . $this->tableNamePrefix . '_hierarchyrelation + DELETE FROM ' . $this->contentGraphTableNames->hierachyRelation() . ' WHERE contentstreamid = :contentStreamId ', [ @@ -641,23 +644,23 @@ private function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): vo // Drop non-referenced nodes (which do not have a hierarchy relation anymore) $this->getDatabaseConnection()->executeUpdate(' - DELETE FROM ' . $this->tableNamePrefix . '_node + DELETE FROM ' . $this->contentGraphTableNames->node() . ' WHERE NOT EXISTS ( - SELECT 1 FROM ' . $this->tableNamePrefix . '_hierarchyrelation - WHERE ' . $this->tableNamePrefix . '_hierarchyrelation.childnodeanchor - = ' . $this->tableNamePrefix . '_node.relationanchorpoint + SELECT 1 FROM ' . $this->contentGraphTableNames->hierachyRelation() . ' + WHERE ' . $this->contentGraphTableNames->hierachyRelation() . '.childnodeanchor + = ' . $this->contentGraphTableNames->node() . '.relationanchorpoint ) '); // Drop non-referenced reference relations (i.e. because the referenced nodes are gone by now) $this->getDatabaseConnection()->executeUpdate(' - DELETE FROM ' . $this->tableNamePrefix . '_referencerelation + DELETE FROM ' . $this->contentGraphTableNames->referenceRelation() . ' WHERE NOT EXISTS ( - SELECT 1 FROM ' . $this->tableNamePrefix . '_node - WHERE ' . $this->tableNamePrefix . '_node.relationanchorpoint - = ' . $this->tableNamePrefix . '_referencerelation.nodeanchorpoint + SELECT 1 FROM ' . $this->contentGraphTableNames->node() . ' + WHERE ' . $this->contentGraphTableNames->node() . '.relationanchorpoint + = ' . $this->contentGraphTableNames->referenceRelation() . '.nodeanchorpoint ) '); }); @@ -742,7 +745,7 @@ function (NodeRecord $node) use ($eventEnvelope) { ); // remove old - $this->getDatabaseConnection()->delete($this->tableNamePrefix . '_referencerelation', [ + $this->getDatabaseConnection()->delete($this->contentGraphTableNames->referenceRelation(), [ 'nodeanchorpoint' => $nodeAnchorPoint?->value, 'name' => $event->referenceName->value ]); @@ -751,7 +754,7 @@ function (NodeRecord $node) use ($eventEnvelope) { $position = 0; /** @var SerializedNodeReference $reference */ foreach ($event->references as $reference) { - $this->getDatabaseConnection()->insert($this->tableNamePrefix . '_referencerelation', [ + $this->getDatabaseConnection()->insert($this->contentGraphTableNames->referenceRelation(), [ 'name' => $event->referenceName->value, 'position' => $position, 'nodeanchorpoint' => $nodeAnchorPoint?->value, @@ -870,7 +873,7 @@ private function updateNodeRecordWithCopyOnWrite( // IMPORTANT: We need to reconnect BOTH the incoming and outgoing edges. $this->getDatabaseConnection()->executeStatement( ' - UPDATE ' . $this->tableNamePrefix . '_hierarchyrelation h + UPDATE ' . $this->contentGraphTableNames->hierachyRelation() . ' h SET -- if our (copied) node is the child, we update h.childNodeAnchor h.childnodeanchor @@ -914,7 +917,7 @@ private function copyReferenceRelations( NodeRelationAnchorPoint $destinationRelationAnchorPoint ): void { $this->getDatabaseConnection()->executeStatement(' - INSERT INTO ' . $this->tableNamePrefix . '_referencerelation ( + INSERT INTO ' . $this->contentGraphTableNames->referenceRelation() . ' ( nodeanchorpoint, name, position, @@ -926,7 +929,7 @@ private function copyReferenceRelations( ref.position, ref.destinationnodeaggregateid FROM - ' . $this->tableNamePrefix . '_referencerelation ref + ' . $this->contentGraphTableNames->referenceRelation() . ' ref WHERE ref.nodeanchorpoint = :sourceNodeAnchorPoint ', [ 'sourceNodeAnchorPoint' => $sourceRelationAnchorPoint->value, @@ -945,8 +948,8 @@ private function whenDimensionSpacePointWasMoved(DimensionSpacePointWasMoved $ev // 1) originDimensionSpacePoint on Node $rel = $this->getDatabaseConnection()->executeQuery( 'SELECT n.relationanchorpoint, n.origindimensionspacepointhash - FROM ' . $this->tableNamePrefix . '_node n - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h + FROM ' . $this->contentGraphTableNames->node() . ' n + INNER JOIN ' . $this->contentGraphTableNames->hierachyRelation() . ' h ON h.childnodeanchor = n.relationanchorpoint AND h.contentstreamid = :contentStreamId @@ -975,7 +978,7 @@ function (NodeRecord $nodeRecord) use ($event) { // 2) hierarchy relations $this->getDatabaseConnection()->executeStatement( ' - UPDATE ' . $this->tableNamePrefix . '_hierarchyrelation h + UPDATE ' . $this->contentGraphTableNames->hierachyRelation() . ' h SET h.dimensionspacepointhash = :newDimensionSpacePointHash WHERE @@ -999,7 +1002,7 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded // 1) hierarchy relations $this->getDatabaseConnection()->executeStatement( ' - INSERT INTO ' . $this->tableNamePrefix . '_hierarchyrelation ( + INSERT INTO ' . $this->contentGraphTableNames->hierachyRelation() . ' ( parentnodeanchor, childnodeanchor, `name`, @@ -1017,7 +1020,7 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded :newDimensionSpacePointHash AS dimensionspacepointhash, h.contentstreamid FROM - ' . $this->tableNamePrefix . '_hierarchyrelation h + ' . $this->contentGraphTableNames->hierachyRelation() . ' h WHERE h.contentstreamid = :contentStreamId AND h.dimensionspacepointhash = :sourceDimensionSpacePointHash', [ diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php index 06cc35ff431..69fe81ac8f3 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php @@ -17,9 +17,12 @@ class DoctrineDbalContentGraphSchemaBuilder { private const DEFAULT_TEXT_COLLATION = 'utf8mb4_unicode_520_ci'; + private readonly ContentGraphTableNames $contentGraphTableNames; + public function __construct( - private readonly string $tableNamePrefix + string $tableNamePrefix ) { + $this->contentGraphTableNames = ContentGraphTableNames::withPrefix($tableNamePrefix); } public function buildSchema(AbstractSchemaManager $schemaManager): Schema @@ -34,7 +37,7 @@ public function buildSchema(AbstractSchemaManager $schemaManager): Schema private function createNodeTable(): Table { - $table = new Table($this->tableNamePrefix . '_node', [ + $table = new Table($this->contentGraphTableNames->node(), [ DbalSchemaFactory::columnForNodeAnchorPoint('relationanchorpoint')->setAutoincrement(true), DbalSchemaFactory::columnForNodeAggregateId('nodeaggregateid')->setNotnull(false), DbalSchemaFactory::columnForDimensionSpacePointHash('origindimensionspacepointhash')->setNotnull(false), @@ -55,7 +58,7 @@ private function createNodeTable(): Table private function createHierarchyRelationTable(): Table { - $table = new Table($this->tableNamePrefix . '_hierarchyrelation', [ + $table = new Table($this->contentGraphTableNames->hierachyRelation(), [ (new Column('name', Type::getType(Types::STRING)))->setLength(255)->setNotnull(false)->setCustomSchemaOption('charset', 'ascii')->setCustomSchemaOption('collation', 'ascii_general_ci'), (new Column('position', Type::getType(Types::INTEGER)))->setNotnull(true), DbalSchemaFactory::columnForContentStreamId('contentstreamid')->setNotnull(true), @@ -72,7 +75,7 @@ private function createHierarchyRelationTable(): Table private function createDimensionSpacePointsTable(): Table { - $table = new Table($this->tableNamePrefix . '_dimensionspacepoints', [ + $table = new Table($this->contentGraphTableNames->dimensionSpacePoints(), [ DbalSchemaFactory::columnForDimensionSpacePointHash('hash')->setNotnull(true), DbalSchemaFactory::columnForDimensionSpacePoint('dimensionspacepoint')->setNotnull(true) ]); @@ -83,7 +86,7 @@ private function createDimensionSpacePointsTable(): Table private function createReferenceRelationTable(): Table { - $table = new Table($this->tableNamePrefix . '_referencerelation', [ + $table = new Table($this->contentGraphTableNames->referenceRelation(), [ (new Column('name', Type::getType(Types::STRING)))->setLength(255)->setNotnull(true)->setCustomSchemaOption('charset', 'ascii')->setCustomSchemaOption('collation', 'ascii_general_ci'), (new Column('position', Type::getType(Types::INTEGER)))->setNotnull(true), DbalSchemaFactory::columnForNodeAnchorPoint('nodeanchorpoint'), diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php index fbc75314807..698064110de 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php @@ -15,7 +15,6 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection; use Doctrine\DBAL\Connection; -use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\DBAL\Types\Types; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\DimensionSpacePointsRepository; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; @@ -53,6 +52,7 @@ public function __construct( */ public function updateToDatabase(Connection $databaseConnection, string $tableNamePrefix): void { + $databaseConnection->update( $tableNamePrefix . '_node', [ diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php index 1df35d06fa3..6c6ec9531cd 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php @@ -14,16 +14,14 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository; -use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver\Exception as DriverException; use Doctrine\DBAL\Exception as DBALException; use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\DBAL\Result; +use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphAdapter; use Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjection; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRelationAnchorPoint; +use Neos\ContentGraph\DoctrineDbalAdapter\NodeQueryBuilder; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; -use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; -use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; @@ -36,7 +34,6 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Exception\RootNodeAggregateDoesNotExist; -use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -64,6 +61,8 @@ */ final class ContentGraph implements ContentGraphInterface { + private readonly NodeQueryBuilder $nodeQueryBuilder; + /** * @var array */ @@ -76,9 +75,10 @@ public function __construct( private readonly NodeTypeManager $nodeTypeManager, private readonly string $tableNamePrefix ) { + $this->nodeQueryBuilder = new NodeQueryBuilder($this->client->getConnection(), $this->tableNamePrefix); } - final public function getSubgraph( + public function getSubgraph( ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, VisibilityConstraints $visibilityConstraints @@ -126,49 +126,27 @@ public function findRootNodeAggregateByType( )); } - $rootNodeAggregate = $rootNodeAggregates->first(); - - if ($rootNodeAggregate === null) { + if (!$rootNodeAggregates->first()) { throw RootNodeAggregateDoesNotExist::butWasExpectedTo($nodeTypeName); } - return $rootNodeAggregate; + return $rootNodeAggregates->first(); } public function findRootNodeAggregates( ContentStreamId $contentStreamId, FindRootNodeAggregatesFilter $filter, ): NodeAggregates { - $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.contentstreamid, h.name, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') - ->from($this->tableNamePrefix . '_node', 'n') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') - ->where('h.contentstreamid = :contentStreamId') - ->andWhere('h.parentnodeanchor = :rootEdgeParentAnchorId') - ->setParameters([ - 'contentStreamId' => $contentStreamId->value, - 'rootEdgeParentAnchorId' => NodeRelationAnchorPoint::forRootEdge()->value, - ]); - - if ($filter->nodeTypeName !== null) { - $queryBuilder - ->andWhere('n.nodetypename = :nodeTypeName') - ->setParameter('nodeTypeName', $filter->nodeTypeName->value); - } - return NodeAggregates::fromArray(iterator_to_array($this->mapQueryBuilderToNodeAggregates($queryBuilder, $contentStreamId))); + $rootNodeAggregateQueryBuilder = $this->nodeQueryBuilder->buildFindRootNodeAggregatesQuery($contentStreamId, $filter); + return NodeAggregates::fromArray(iterator_to_array($this->mapQueryBuilderToNodeAggregates($rootNodeAggregateQueryBuilder, $contentStreamId))); } public function findNodeAggregatesByType( ContentStreamId $contentStreamId, NodeTypeName $nodeTypeName ): iterable { - $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.contentstreamid, h.name, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') - ->from($this->tableNamePrefix . '_node', 'n') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') - ->where('h.contentstreamid = :contentStreamId') + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeAggregateQuery(); + $queryBuilder ->andWhere('n.nodetypename = :nodeTypeName') ->setParameters([ 'contentStreamId' => $contentStreamId->value, @@ -181,23 +159,8 @@ public function findNodeAggregateById( ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId ): ?NodeAggregate { - $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') - ->from($this->tableNamePrefix . '_hierarchyrelation', 'h') - ->innerJoin('h', $this->tableNamePrefix . '_node', 'n', 'n.relationanchorpoint = h.childnodeanchor') - ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') - ->where('n.nodeaggregateid = :nodeAggregateId') - ->andWhere('h.contentstreamid = :contentStreamId') - ->setParameters([ - 'nodeAggregateId' => $nodeAggregateId->value, - 'contentStreamId' => $contentStreamId->value - ]); - - return $this->nodeFactory->mapNodeRowsToNodeAggregate( - $this->fetchRows($queryBuilder), - $contentStreamId, - VisibilityConstraints::withoutRestrictions() - ); + $contentGraphAdapter = new ContentGraphAdapter($this->client->getConnection(), $this->tableNamePrefix, $this->contentRepositoryId, $this->nodeFactory, null, $contentStreamId); + return $contentGraphAdapter->findNodeAggregateById($nodeAggregateId); } /** @@ -207,57 +170,8 @@ public function findParentNodeAggregates( ContentStreamId $contentStreamId, NodeAggregateId $childNodeAggregateId ): iterable { - $queryBuilder = $this->createQueryBuilder() - ->select('pn.*, ph.name, ph.contentstreamid, ph.subtreetags, pdsp.dimensionspacepoint AS covereddimensionspacepoint') - ->from($this->tableNamePrefix . '_node', 'pn') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = pn.relationanchorpoint') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') - ->innerJoin('ch', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') - ->innerJoin('ph', $this->tableNamePrefix . '_dimensionspacepoints', 'pdsp', 'pdsp.hash = ph.dimensionspacepointhash') - ->where('cn.nodeaggregateid = :nodeAggregateId') - ->andWhere('ph.contentstreamid = :contentStreamId') - ->andWhere('ch.contentstreamid = :contentStreamId') - ->setParameters([ - 'nodeAggregateId' => $childNodeAggregateId->value, - 'contentStreamId' => $contentStreamId->value - ]); - - return $this->mapQueryBuilderToNodeAggregates($queryBuilder, $contentStreamId); - } - - public function findParentNodeAggregateByChildOriginDimensionSpacePoint( - ContentStreamId $contentStreamId, - NodeAggregateId $childNodeAggregateId, - OriginDimensionSpacePoint $childOriginDimensionSpacePoint - ): ?NodeAggregate { - $subQueryBuilder = $this->createQueryBuilder() - ->select('pn.nodeaggregateid') - ->from($this->tableNamePrefix . '_node', 'pn') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') - ->innerJoin('ch', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') - ->where('ch.contentstreamid = :contentStreamId') - ->andWhere('ch.dimensionspacepointhash = :childOriginDimensionSpacePointHash') - ->andWhere('cn.nodeaggregateid = :childNodeAggregateId') - ->andWhere('cn.origindimensionspacepointhash = :childOriginDimensionSpacePointHash'); - - $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') - ->from($this->tableNamePrefix . '_node', 'n') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') - ->where('n.nodeaggregateid = (' . $subQueryBuilder->getSQL() . ')') - ->andWhere('h.contentstreamid = :contentStreamId') - ->setParameters([ - 'contentStreamId' => $contentStreamId->value, - 'childNodeAggregateId' => $childNodeAggregateId->value, - 'childOriginDimensionSpacePointHash' => $childOriginDimensionSpacePoint->hash, - ]); - - return $this->nodeFactory->mapNodeRowsToNodeAggregate( - $this->fetchRows($queryBuilder), - $contentStreamId, - VisibilityConstraints::withoutRestrictions() - ); + $contentGraphAdapter = new ContentGraphAdapter($this->client->getConnection(), $this->tableNamePrefix, $this->contentRepositoryId, $this->nodeFactory, null, $contentStreamId); + return $contentGraphAdapter->findParentNodeAggregates($childNodeAggregateId); } /** @@ -267,8 +181,8 @@ public function findChildNodeAggregates( ContentStreamId $contentStreamId, NodeAggregateId $parentNodeAggregateId ): iterable { - $queryBuilder = $this->buildChildNodeAggregateQuery($parentNodeAggregateId, $contentStreamId); - return $this->mapQueryBuilderToNodeAggregates($queryBuilder, $contentStreamId); + $contentGraphAdapter = new ContentGraphAdapter($this->client->getConnection(), $this->tableNamePrefix, $this->contentRepositoryId, $this->nodeFactory, null, $contentStreamId); + return $contentGraphAdapter->findChildNodeAggregates($parentNodeAggregateId); } /** @@ -279,73 +193,15 @@ public function findChildNodeAggregatesByName( NodeAggregateId $parentNodeAggregateId, NodeName $name ): iterable { - $queryBuilder = $this->buildChildNodeAggregateQuery($parentNodeAggregateId, $contentStreamId) - ->andWhere('ch.name = :relationName') - ->setParameter('relationName', $name->value); - return $this->mapQueryBuilderToNodeAggregates($queryBuilder, $contentStreamId); - } - - /** - * @return iterable - */ - public function findTetheredChildNodeAggregates( - ContentStreamId $contentStreamId, - NodeAggregateId $parentNodeAggregateId - ): iterable { - $queryBuilder = $this->buildChildNodeAggregateQuery($parentNodeAggregateId, $contentStreamId) - ->andWhere('cn.classification = :tetheredClassification') - ->setParameter('tetheredClassification', NodeAggregateClassification::CLASSIFICATION_TETHERED->value); - return $this->mapQueryBuilderToNodeAggregates($queryBuilder, $contentStreamId); - } - - /** - * @param ContentStreamId $contentStreamId - * @param NodeName $nodeName - * @param NodeAggregateId $parentNodeAggregateId - * @param OriginDimensionSpacePoint $parentNodeOriginDimensionSpacePoint - * @param DimensionSpacePointSet $dimensionSpacePointsToCheck - * @return DimensionSpacePointSet - */ - public function getDimensionSpacePointsOccupiedByChildNodeName( - ContentStreamId $contentStreamId, - NodeName $nodeName, - NodeAggregateId $parentNodeAggregateId, - OriginDimensionSpacePoint $parentNodeOriginDimensionSpacePoint, - DimensionSpacePointSet $dimensionSpacePointsToCheck - ): DimensionSpacePointSet { - $queryBuilder = $this->createQueryBuilder() - ->select('dsp.dimensionspacepoint, h.dimensionspacepointhash') - ->from($this->tableNamePrefix . '_hierarchyrelation', 'h') - ->innerJoin('h', $this->tableNamePrefix . '_node', 'n', 'n.relationanchorpoint = h.parentnodeanchor') - ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = n.relationanchorpoint') - ->where('n.nodeaggregateid = :parentNodeAggregateId') - ->andWhere('n.origindimensionspacepointhash = :parentNodeOriginDimensionSpacePointHash') - ->andWhere('ph.contentstreamid = :contentStreamId') - ->andWhere('h.contentstreamid = :contentStreamId') - ->andWhere('h.dimensionspacepointhash IN (:dimensionSpacePointHashes)') - ->andWhere('h.name = :nodeName') - ->setParameters([ - 'parentNodeAggregateId' => $parentNodeAggregateId->value, - 'parentNodeOriginDimensionSpacePointHash' => $parentNodeOriginDimensionSpacePoint->hash, - 'contentStreamId' => $contentStreamId->value, - 'dimensionSpacePointHashes' => $dimensionSpacePointsToCheck->getPointHashes(), - 'nodeName' => $nodeName->value - ], [ - 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY, - ]); - $dimensionSpacePoints = []; - foreach ($this->fetchRows($queryBuilder) as $hierarchyRelationData) { - $dimensionSpacePoints[$hierarchyRelationData['dimensionspacepointhash']] = DimensionSpacePoint::fromJsonString($hierarchyRelationData['dimensionspacepoint']); - } - return new DimensionSpacePointSet($dimensionSpacePoints); + $contentGraphAdapter = new ContentGraphAdapter($this->client->getConnection(), $this->tableNamePrefix, $this->contentRepositoryId, $this->nodeFactory, null, $contentStreamId); + return $contentGraphAdapter->findChildNodeAggregatesByName($parentNodeAggregateId, $name); } public function countNodes(): int { $queryBuilder = $this->createQueryBuilder() ->select('COUNT(*)') - ->from($this->tableNamePrefix . '_node'); + ->from($this->nodeQueryBuilder->contentGraphTableNames->node()); $result = $queryBuilder->execute(); if (!$result instanceof Result) { throw new \RuntimeException(sprintf('Failed to count nodes. Expected result to be of type %s, got: %s', Result::class, get_debug_type($result)), 1701444550); @@ -359,10 +215,7 @@ public function countNodes(): int public function findUsedNodeTypeNames(): iterable { - $rows = $this->fetchRows($this->createQueryBuilder() - ->select('DISTINCT nodetypename') - ->from($this->tableNamePrefix . '_node')); - return array_map(static fn (array $row) => NodeTypeName::fromString($row['nodetypename']), $rows); + return array_map(static fn (array $row) => NodeTypeName::fromString($row['nodetypename']), $this->nodeQueryBuilder->findUsedNodeTypeNames()); } /** @@ -374,25 +227,6 @@ public function getSubgraphs(): array return $this->subgraphs; } - private function buildChildNodeAggregateQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamId $contentStreamId): QueryBuilder - { - return $this->createQueryBuilder() - ->select('cn.*, ch.name, ch.contentstreamid, ch.subtreetags, cdsp.dimensionspacepoint AS covereddimensionspacepoint') - ->from($this->tableNamePrefix . '_node', 'pn') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = pn.relationanchorpoint') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') - ->innerJoin('ch', $this->tableNamePrefix . '_dimensionspacepoints', 'cdsp', 'cdsp.hash = ch.dimensionspacepointhash') - ->innerJoin('ch', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') - ->where('pn.nodeaggregateid = :parentNodeAggregateId') - ->andWhere('ph.contentstreamid = :contentStreamId') - ->andWhere('ch.contentstreamid = :contentStreamId') - ->orderBy('ch.position') - ->setParameters([ - 'parentNodeAggregateId' => $parentNodeAggregateId->value, - 'contentStreamId' => $contentStreamId->value, - ]); - } - private function createQueryBuilder(): QueryBuilder { return $this->client->getConnection()->createQueryBuilder(); diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php index c1a278822d7..d2ad6a3008a 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php @@ -14,12 +14,11 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository; -use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver\Exception as DbalDriverException; use Doctrine\DBAL\Exception as DbalException; use Doctrine\DBAL\ForwardCompatibility\Result; -use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Query\QueryBuilder; +use Neos\ContentGraph\DoctrineDbalAdapter\NodeQueryBuilder; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; @@ -42,28 +41,14 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSubtreeFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSucceedingSiblingNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\ExpandedNodeTypeCriteria; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Ordering\Ordering; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Ordering\OrderingDirection; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Ordering\TimestampField; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Pagination\Pagination; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\AndCriteria; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\NegateCriteria; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\OrCriteria; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueContains; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueCriteriaInterface; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueEndsWith; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueEquals; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueGreaterThan; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueGreaterThanOrEqual; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueLessThan; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueLessThanOrEqual; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueStartsWith; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; use Neos\ContentRepository\Core\Projection\ContentGraph\References; -use Neos\ContentRepository\Core\Projection\ContentGraph\SearchTerm; use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; @@ -101,7 +86,7 @@ */ final class ContentSubgraph implements ContentSubgraphInterface { - private int $dynamicParameterCount = 0; + private readonly NodeQueryBuilder $nodeQueryBuilder; public function __construct( private readonly ContentRepositoryId $contentRepositoryId, @@ -111,8 +96,9 @@ public function __construct( private readonly DbalClientInterface $client, private readonly NodeFactory $nodeFactory, private readonly NodeTypeManager $nodeTypeManager, - private readonly string $tableNamePrefix + string $tableNamePrefix ) { + $this->nodeQueryBuilder = new NodeQueryBuilder($this->client->getConnection(), $tableNamePrefix); } public function getIdentity(): ContentSubgraphIdentity @@ -168,45 +154,23 @@ public function countBackReferences(NodeAggregateId $nodeAggregateId, CountBackR public function findNodeById(NodeAggregateId $nodeAggregateId): ?Node { - $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.subtreetags') - ->from($this->tableNamePrefix . '_node', 'n') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->where('n.nodeaggregateid = :nodeAggregateId')->setParameter('nodeAggregateId', $nodeAggregateId->value) - ->andWhere('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash); + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeByIdQuery($nodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint); $this->addSubtreeTagConstraints($queryBuilder); return $this->fetchNode($queryBuilder); } public function findRootNodeByType(NodeTypeName $nodeTypeName): ?Node { - $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.subtreetags') - ->from($this->tableNamePrefix . '_node', 'n') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->where('n.nodetypename = :nodeTypeName')->setParameter('nodeTypeName', $nodeTypeName->value) - ->andWhere('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->andWhere('n.classification = :nodeAggregateClassification') - ->setParameter('nodeAggregateClassification', NodeAggregateClassification::CLASSIFICATION_ROOT->value); + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamId, $this->dimensionSpacePoint) + ->andWhere('n.nodetypename = :nodeTypeName')->setParameter('nodeTypeName', $nodeTypeName->value) + ->andWhere('n.classification = :nodeAggregateClassification')->setParameter('nodeAggregateClassification', NodeAggregateClassification::CLASSIFICATION_ROOT->value); $this->addSubtreeTagConstraints($queryBuilder); return $this->fetchNode($queryBuilder); } public function findParentNode(NodeAggregateId $childNodeAggregateId): ?Node { - $queryBuilder = $this->createQueryBuilder() - ->select('pn.*, ch.name, ch.subtreetags') - ->from($this->tableNamePrefix . '_node', 'pn') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.parentnodeanchor = pn.relationanchorpoint') - ->innerJoin('pn', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ph.childnodeanchor') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.childnodeanchor = pn.relationanchorpoint') - ->where('cn.nodeaggregateid = :childNodeAggregateId')->setParameter('childNodeAggregateId', $childNodeAggregateId->value) - ->andWhere('ph.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) - ->andWhere('ch.contentstreamid = :contentStreamId') - ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->andWhere('ch.dimensionspacepointhash = :dimensionSpacePointHash'); + $queryBuilder = $this->nodeQueryBuilder->buildBasicParentNodeQuery($childNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint); $this->addSubtreeTagConstraints($queryBuilder, 'ph'); return $this->fetchNode($queryBuilder); } @@ -238,14 +202,7 @@ public function findNodeByAbsolutePath(AbsoluteNodePath $path): ?Node */ private function findChildNodeConnectedThroughEdgeName(NodeAggregateId $parentNodeAggregateId, NodeName $nodeName): ?Node { - $queryBuilder = $this->createQueryBuilder() - ->select('cn.*, h.name, h.subtreetags') - ->from($this->tableNamePrefix . '_node', 'pn') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = pn.relationanchorpoint') - ->innerJoin('pn', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = h.childnodeanchor') - ->where('pn.nodeaggregateid = :parentNodeAggregateId')->setParameter('parentNodeAggregateId', $parentNodeAggregateId->value) - ->andWhere('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) + $queryBuilder = $this->nodeQueryBuilder->buildBasicChildNodesQuery($parentNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint) ->andWhere('h.name = :edgeName')->setParameter('edgeName', $nodeName->value); $this->addSubtreeTagConstraints($queryBuilder); return $this->fetchNode($queryBuilder); @@ -291,8 +248,8 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi $queryBuilderInitial = $this->createQueryBuilder() // @see https://mariadb.com/kb/en/library/recursive-common-table-expressions-overview/#cast-to-avoid-data-truncation ->select('n.*, h.name, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') - ->from($this->tableNamePrefix . '_node', 'n') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') + ->from($this->nodeQueryBuilder->contentGraphTableNames->node(), 'n') + ->innerJoin('n', $this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('n.nodeaggregateid = :entryNodeAggregateId'); @@ -301,15 +258,15 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi $queryBuilderRecursive = $this->createQueryBuilder() ->select('c.*, h.name, h.subtreetags, p.nodeaggregateid AS parentNodeAggregateId, p.level + 1 AS level, h.position') ->from('tree', 'p') - ->innerJoin('p', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = p.relationanchorpoint') - ->innerJoin('p', $this->tableNamePrefix . '_node', 'c', 'c.relationanchorpoint = h.childnodeanchor') + ->innerJoin('p', $this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'h', 'h.parentnodeanchor = p.relationanchorpoint') + ->innerJoin('p', $this->nodeQueryBuilder->contentGraphTableNames->node(), 'c', 'c.relationanchorpoint = h.childnodeanchor') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); if ($filter->maximumLevels !== null) { $queryBuilderRecursive->andWhere('p.level < :maximumLevels')->setParameter('maximumLevels', $filter->maximumLevels); } if ($filter->nodeTypes !== null) { - $this->addNodeTypeCriteria($queryBuilderRecursive, $filter->nodeTypes, 'c'); + $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderRecursive, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), 'c'); } $this->addSubtreeTagConstraints($queryBuilderRecursive); @@ -383,9 +340,9 @@ public function findClosestNode(NodeAggregateId $entryNodeAggregateId, FindClose { $queryBuilderInitial = $this->createQueryBuilder() ->select('n.*, ph.name, ph.subtreetags, ph.parentnodeanchor') - ->from($this->tableNamePrefix . '_node', 'n') + ->from($this->nodeQueryBuilder->contentGraphTableNames->node(), 'n') // we need to join with the hierarchy relation, because we need the node name. - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'n.relationanchorpoint = ph.childnodeanchor') + ->innerJoin('n', $this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'ph', 'n.relationanchorpoint = ph.childnodeanchor') ->andWhere('ph.contentstreamid = :contentStreamId') ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('n.nodeaggregateid = :entryNodeAggregateId'); @@ -394,21 +351,15 @@ public function findClosestNode(NodeAggregateId $entryNodeAggregateId, FindClose $queryBuilderRecursive = $this->createQueryBuilder() ->select('pn.*, h.name, h.subtreetags, h.parentnodeanchor') ->from('ancestry', 'cn') - ->innerJoin('cn', $this->tableNamePrefix . '_node', 'pn', 'pn.relationanchorpoint = cn.parentnodeanchor') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = pn.relationanchorpoint') + ->innerJoin('cn', $this->nodeQueryBuilder->contentGraphTableNames->node(), 'pn', 'pn.relationanchorpoint = cn.parentnodeanchor') + ->innerJoin('pn', $this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'h', 'h.childnodeanchor = pn.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); $this->addSubtreeTagConstraints($queryBuilderRecursive); - $queryBuilderCte = $this->createQueryBuilder() - ->select('*') - ->from('ancestry', 'pn') - ->setMaxResults(1) - ->setParameter('contentStreamId', $this->contentStreamId->value) - ->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); + $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint); if ($filter->nodeTypes !== null) { - $this->addNodeTypeCriteria($queryBuilderCte, $filter->nodeTypes, 'pn'); + $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderCte, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), 'pn'); } $nodeRows = $this->fetchCteResults( $queryBuilderInitial, @@ -446,21 +397,18 @@ public function countDescendantNodes(NodeAggregateId $entryNodeAggregateId, Coun public function countNodes(): int { - $queryBuilder = $this->createQueryBuilder() - ->select('COUNT(*)') - ->from($this->tableNamePrefix . '_node', 'n') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->where('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash); + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamId, $this->dimensionSpacePoint, 'n', 'COUNT(*)'); try { $result = $this->executeQuery($queryBuilder)->fetchOne(); - if (!is_int($result)) { - throw new \RuntimeException(sprintf('Expected result to be of type integer but got: %s', get_debug_type($result)), 1678366902); - } - return $result; } catch (DbalDriverException | DbalException $e) { throw new \RuntimeException(sprintf('Failed to count all nodes: %s', $e->getMessage()), 1678364741, $e); } + + if (!is_int($result)) { + throw new \RuntimeException(sprintf('Expected result to be of type integer but got: %s', get_debug_type($result)), 1678366902); + } + + return $result; } public function jsonSerialize(): ContentSubgraphIdentity @@ -488,12 +436,6 @@ private function createQueryBuilder(): QueryBuilder return $this->client->getConnection()->createQueryBuilder(); } - private function createUniqueParameterName(): string - { - return 'param_' . (++$this->dynamicParameterCount); - } - - private function addSubtreeTagConstraints(QueryBuilder $queryBuilder, string $hierarchyRelationTableAlias = 'h'): void { $hierarchyRelationTablePrefix = $hierarchyRelationTableAlias === '' ? '' : $hierarchyRelationTableAlias . '.'; @@ -504,119 +446,17 @@ private function addSubtreeTagConstraints(QueryBuilder $queryBuilder, string $hi } } - private function addNodeTypeCriteria(QueryBuilder $queryBuilder, NodeTypeCriteria $nodeTypeCriteria, string $nodeTableAlias = 'n'): void - { - $nodeTablePrefix = $nodeTableAlias === '' ? '' : $nodeTableAlias . '.'; - $constraintsWithSubNodeTypes = ExpandedNodeTypeCriteria::create($nodeTypeCriteria, $this->nodeTypeManager); - $allowanceQueryPart = ''; - if (!$constraintsWithSubNodeTypes->explicitlyAllowedNodeTypeNames->isEmpty()) { - $allowanceQueryPart = $queryBuilder->expr()->in($nodeTablePrefix . 'nodetypename', ':explicitlyAllowedNodeTypeNames'); - $queryBuilder->setParameter('explicitlyAllowedNodeTypeNames', $constraintsWithSubNodeTypes->explicitlyAllowedNodeTypeNames->toStringArray(), Connection::PARAM_STR_ARRAY); - } - $denyQueryPart = ''; - if (!$constraintsWithSubNodeTypes->explicitlyDisallowedNodeTypeNames->isEmpty()) { - $denyQueryPart = $queryBuilder->expr()->notIn($nodeTablePrefix . 'nodetypename', ':explicitlyDisallowedNodeTypeNames'); - $queryBuilder->setParameter('explicitlyDisallowedNodeTypeNames', $constraintsWithSubNodeTypes->explicitlyDisallowedNodeTypeNames->toStringArray(), Connection::PARAM_STR_ARRAY); - } - if ($allowanceQueryPart && $denyQueryPart) { - if ($constraintsWithSubNodeTypes->isWildCardAllowed) { - $queryBuilder->andWhere($queryBuilder->expr()->or($allowanceQueryPart, $denyQueryPart)); - } else { - $queryBuilder->andWhere($queryBuilder->expr()->and($allowanceQueryPart, $denyQueryPart)); - } - } elseif ($allowanceQueryPart && !$constraintsWithSubNodeTypes->isWildCardAllowed) { - $queryBuilder->andWhere($allowanceQueryPart); - } elseif ($denyQueryPart) { - $queryBuilder->andWhere($denyQueryPart); - } - } - - private function addSearchTermConstraints(QueryBuilder $queryBuilder, SearchTerm $searchTerm, string $nodeTableAlias = 'n'): void - { - $queryBuilder->andWhere('JSON_SEARCH(' . $nodeTableAlias . '.properties, "one", :searchTermPattern, NULL, "$.*.value") IS NOT NULL')->setParameter('searchTermPattern', '%' . $searchTerm->term . '%'); - } - - private function addPropertyValueConstraints(QueryBuilder $queryBuilder, PropertyValueCriteriaInterface $propertyValue, string $nodeTableAlias = 'n'): void - { - $queryBuilder->andWhere($this->propertyValueConstraints($queryBuilder, $propertyValue, $nodeTableAlias)); - } - - private function propertyValueConstraints(QueryBuilder $queryBuilder, PropertyValueCriteriaInterface $propertyValue, string $nodeTableAlias): string - { - return match ($propertyValue::class) { - AndCriteria::class => (string)$queryBuilder->expr()->and($this->propertyValueConstraints($queryBuilder, $propertyValue->criteria1, $nodeTableAlias), $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria2, $nodeTableAlias)), - NegateCriteria::class => 'NOT (' . $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria, $nodeTableAlias) . ')', - OrCriteria::class => (string)$queryBuilder->expr()->or($this->propertyValueConstraints($queryBuilder, $propertyValue->criteria1, $nodeTableAlias), $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria2, $nodeTableAlias)), - PropertyValueContains::class => $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, '%' . $propertyValue->value . '%', $nodeTableAlias, $propertyValue->caseSensitive), - PropertyValueEndsWith::class => $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, '%' . $propertyValue->value, $nodeTableAlias, $propertyValue->caseSensitive), - PropertyValueEquals::class => is_string($propertyValue->value) ? $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, $nodeTableAlias, $propertyValue->caseSensitive) : $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '=', $nodeTableAlias), - PropertyValueGreaterThan::class => $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '>', $nodeTableAlias), - PropertyValueGreaterThanOrEqual::class => $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '>=', $nodeTableAlias), - PropertyValueLessThan::class => $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '<', $nodeTableAlias), - PropertyValueLessThanOrEqual::class => $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '<=', $nodeTableAlias), - PropertyValueStartsWith::class => $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value . '%', $nodeTableAlias, $propertyValue->caseSensitive), - default => throw new \InvalidArgumentException(sprintf('Invalid/unsupported property value criteria "%s"', get_debug_type($propertyValue)), 1679561062), - }; - } - - private function comparePropertyValueStatement(QueryBuilder $queryBuilder, PropertyName $propertyName, string|int|float|bool $value, string $operator, string $nodeTableAlias): string - { - $paramName = $this->createUniqueParameterName(); - $paramType = match (gettype($value)) { - 'boolean' => ParameterType::BOOLEAN, - 'integer' => ParameterType::INTEGER, - default => ParameterType::STRING, - }; - $queryBuilder->setParameter($paramName, $value, $paramType); - return $this->extractPropertyValue($propertyName, $nodeTableAlias) . ' ' . $operator . ' :' . $paramName; - } - - private function extractPropertyValue(PropertyName $propertyName, string $nodeTableAlias): string - { - try { - $escapedPropertyName = addslashes(json_encode($propertyName->value, JSON_THROW_ON_ERROR)); - } catch (\JsonException $e) { - throw new \RuntimeException(sprintf('Failed to escape property name: %s', $e->getMessage()), 1679394579, $e); - } - return 'JSON_EXTRACT(' . $nodeTableAlias . '.properties, \'$.' . $escapedPropertyName . '.value\')'; - } - - private function searchPropertyValueStatement(QueryBuilder $queryBuilder, PropertyName $propertyName, string|bool|int|float $value, string $nodeTableAlias, bool $caseSensitive): string - { - try { - $escapedPropertyName = addslashes(json_encode($propertyName->value, JSON_THROW_ON_ERROR)); - } catch (\JsonException $e) { - throw new \RuntimeException(sprintf('Failed to escape property name: %s', $e->getMessage()), 1679394579, $e); - } - if (is_bool($value)) { - return 'JSON_SEARCH(' . $nodeTableAlias . '.properties, \'one\', \'' . ($value ? 'true' : 'false') . '\', NULL, \'$.' . $escapedPropertyName . '.value\') IS NOT NULL'; - } - $paramName = $this->createUniqueParameterName(); - $queryBuilder->setParameter($paramName, $value); - if ($caseSensitive) { - return 'JSON_SEARCH(' . $nodeTableAlias . '.properties COLLATE utf8mb4_bin, \'one\', :' . $paramName . ' COLLATE utf8mb4_bin, NULL, \'$.' . $escapedPropertyName . '.value\') IS NOT NULL'; - } - return 'JSON_SEARCH(' . $nodeTableAlias . '.properties, \'one\', :' . $paramName . ', NULL, \'$.' . $escapedPropertyName . '.value\') IS NOT NULL'; - } - private function buildChildNodesQuery(NodeAggregateId $parentNodeAggregateId, FindChildNodesFilter|CountChildNodesFilter $filter): QueryBuilder { - $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.subtreetags') - ->from($this->tableNamePrefix . '_node', 'pn') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = pn.relationanchorpoint') - ->innerJoin('pn', $this->tableNamePrefix . '_node', 'n', 'h.childnodeanchor = n.relationanchorpoint') - ->where('pn.nodeaggregateid = :parentNodeAggregateId')->setParameter('parentNodeAggregateId', $parentNodeAggregateId->value) - ->andWhere('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash); + $queryBuilder = $this->nodeQueryBuilder->buildBasicChildNodesQuery($parentNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint); if ($filter->nodeTypes !== null) { - $this->addNodeTypeCriteria($queryBuilder, $filter->nodeTypes); + $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager)); } if ($filter->searchTerm !== null) { - $this->addSearchTermConstraints($queryBuilder, $filter->searchTerm); + $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->searchTerm); } if ($filter->propertyValue !== null) { - $this->addPropertyValueConstraints($queryBuilder, $filter->propertyValue); + $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->propertyValue); } $this->addSubtreeTagConstraints($queryBuilder); return $queryBuilder; @@ -628,11 +468,11 @@ private function buildReferencesQuery(bool $backReferences, NodeAggregateId $nod $destinationTablePrefix = $backReferences ? 's' : 'd'; $queryBuilder = $this->createQueryBuilder() ->select("{$destinationTablePrefix}n.*, {$destinationTablePrefix}h.name, {$destinationTablePrefix}h.subtreetags, r.name AS referencename, r.properties AS referenceproperties") - ->from($this->tableNamePrefix . '_hierarchyrelation', 'sh') - ->innerJoin('sh', $this->tableNamePrefix . '_node', 'sn', 'sn.relationanchorpoint = sh.childnodeanchor') - ->innerJoin('sh', $this->tableNamePrefix . '_referencerelation', 'r', 'r.nodeanchorpoint = sn.relationanchorpoint') - ->innerJoin('sh', $this->tableNamePrefix . '_node', 'dn', 'dn.nodeaggregateid = r.destinationnodeaggregateid') - ->innerJoin('sh', $this->tableNamePrefix . '_hierarchyrelation', 'dh', 'dh.childnodeanchor = dn.relationanchorpoint') + ->from($this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'sh') + ->innerJoin('sh', $this->nodeQueryBuilder->contentGraphTableNames->node(), 'sn', 'sn.relationanchorpoint = sh.childnodeanchor') + ->innerJoin('sh', $this->nodeQueryBuilder->contentGraphTableNames->referenceRelation(), 'r', 'r.nodeanchorpoint = sn.relationanchorpoint') + ->innerJoin('sh', $this->nodeQueryBuilder->contentGraphTableNames->node(), 'dn', 'dn.nodeaggregateid = r.destinationnodeaggregateid') + ->innerJoin('sh', $this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'dh', 'dh.childnodeanchor = dn.relationanchorpoint') ->where("{$sourceTablePrefix}n.nodeaggregateid = :nodeAggregateId")->setParameter('nodeAggregateId', $nodeAggregateId->value) ->andWhere('dh.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) ->andWhere('sh.dimensionspacepointhash = :dimensionSpacePointHash') @@ -641,22 +481,19 @@ private function buildReferencesQuery(bool $backReferences, NodeAggregateId $nod $this->addSubtreeTagConstraints($queryBuilder, 'dh'); $this->addSubtreeTagConstraints($queryBuilder, 'sh'); if ($filter->nodeTypes !== null) { - $this->addNodeTypeCriteria($queryBuilder, $filter->nodeTypes, "{$destinationTablePrefix}n"); + $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), "{$destinationTablePrefix}n"); } if ($filter->nodeSearchTerm !== null) { - $this->addSearchTermConstraints($queryBuilder, $filter->nodeSearchTerm, "{$destinationTablePrefix}n"); + $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->nodeSearchTerm, "{$destinationTablePrefix}n"); } if ($filter->nodePropertyValue !== null) { - $this->addPropertyValueConstraints($queryBuilder, $filter->nodePropertyValue, "{$destinationTablePrefix}n"); + $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->nodePropertyValue, "{$destinationTablePrefix}n"); } if ($filter->referenceSearchTerm !== null) { - $this->addSearchTermConstraints($queryBuilder, $filter->referenceSearchTerm, 'r'); + $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->referenceSearchTerm, 'r'); } if ($filter->referencePropertyValue !== null) { - $this->addPropertyValueConstraints($queryBuilder, $filter->referencePropertyValue, 'r'); - } - if ($filter->nodePropertyValue !== null) { - $this->addPropertyValueConstraints($queryBuilder, $filter->nodePropertyValue, "{$destinationTablePrefix}n"); + $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->referencePropertyValue, 'r'); } if ($filter->referenceName !== null) { $queryBuilder->andWhere('r.name = :referenceName')->setParameter('referenceName', $filter->referenceName->value); @@ -678,42 +515,17 @@ private function buildReferencesQuery(bool $backReferences, NodeAggregateId $nod private function buildSiblingsQuery(bool $preceding, NodeAggregateId $siblingNodeAggregateId, FindPrecedingSiblingNodesFilter|FindSucceedingSiblingNodesFilter $filter): QueryBuilder { - $parentNodeAnchorSubQuery = $this->createQueryBuilder() - ->select('sh.parentnodeanchor') - ->from($this->tableNamePrefix . '_hierarchyrelation', 'sh') - ->innerJoin('sh', $this->tableNamePrefix . '_node', 'sn', 'sn.relationanchorpoint = sh.childnodeanchor') - ->where('sn.nodeaggregateid = :siblingNodeAggregateId') - ->andWhere('sh.contentstreamid = :contentStreamId') - ->andWhere('sh.dimensionspacepointhash = :dimensionSpacePointHash'); - - $siblingPositionSubQuery = $this->createQueryBuilder() - ->select('sh.position') - ->from($this->tableNamePrefix . '_hierarchyrelation', 'sh') - ->innerJoin('sh', $this->tableNamePrefix . '_node', 'sn', 'sn.relationanchorpoint = sh.childnodeanchor') - ->where('sn.nodeaggregateid = :siblingNodeAggregateId') - ->andWhere('sh.contentstreamid = :contentStreamId') - ->andWhere('sh.dimensionspacepointhash = :dimensionSpacePointHash'); - - $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.subtreetags') - ->from($this->tableNamePrefix . '_node', 'n') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->where('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->andWhere('h.parentnodeanchor = (' . $parentNodeAnchorSubQuery->getSQL() . ')') - ->andWhere('n.nodeaggregateid != :siblingNodeAggregateId')->setParameter('siblingNodeAggregateId', $siblingNodeAggregateId->value) - ->andWhere('h.position ' . ($preceding ? '<' : '>') . ' (' . $siblingPositionSubQuery->getSQL() . ')') - ->orderBy('h.position', $preceding ? 'DESC' : 'ASC'); + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeSiblingsQuery($preceding, $siblingNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint); $this->addSubtreeTagConstraints($queryBuilder); if ($filter->nodeTypes !== null) { - $this->addNodeTypeCriteria($queryBuilder, $filter->nodeTypes); + $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager)); } if ($filter->searchTerm !== null) { - $this->addSearchTermConstraints($queryBuilder, $filter->searchTerm); + $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->searchTerm); } if ($filter->propertyValue !== null) { - $this->addPropertyValueConstraints($queryBuilder, $filter->propertyValue); + $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->propertyValue); } if ($filter->pagination !== null) { $this->applyPagination($queryBuilder, $filter->pagination); @@ -728,11 +540,11 @@ private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId { $queryBuilderInitial = $this->createQueryBuilder() ->select('n.*, ph.name, ph.subtreetags, ph.parentnodeanchor') - ->from($this->tableNamePrefix . '_node', 'n') + ->from($this->nodeQueryBuilder->contentGraphTableNames->node(), 'n') // we need to join with the hierarchy relation, because we need the node name. - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = n.relationanchorpoint') - ->innerJoin('ch', $this->tableNamePrefix . '_node', 'c', 'c.relationanchorpoint = ch.childnodeanchor') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'n.relationanchorpoint = ph.childnodeanchor') + ->innerJoin('n', $this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'ch', 'ch.parentnodeanchor = n.relationanchorpoint') + ->innerJoin('ch', $this->nodeQueryBuilder->contentGraphTableNames->node(), 'c', 'c.relationanchorpoint = ch.childnodeanchor') + ->innerJoin('n', $this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'ph', 'n.relationanchorpoint = ph.childnodeanchor') ->where('ch.contentstreamid = :contentStreamId') ->andWhere('ch.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('ph.contentstreamid = :contentStreamId') @@ -744,20 +556,15 @@ private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId $queryBuilderRecursive = $this->createQueryBuilder() ->select('pn.*, h.name, h.subtreetags, h.parentnodeanchor') ->from('ancestry', 'cn') - ->innerJoin('cn', $this->tableNamePrefix . '_node', 'pn', 'pn.relationanchorpoint = cn.parentnodeanchor') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = pn.relationanchorpoint') + ->innerJoin('cn', $this->nodeQueryBuilder->contentGraphTableNames->node(), 'pn', 'pn.relationanchorpoint = cn.parentnodeanchor') + ->innerJoin('pn', $this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'h', 'h.childnodeanchor = pn.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); $this->addSubtreeTagConstraints($queryBuilderRecursive); - $queryBuilderCte = $this->createQueryBuilder() - ->select('*') - ->from('ancestry', 'pn') - ->setParameter('contentStreamId', $this->contentStreamId->value) - ->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); + $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint); if ($filter->nodeTypes !== null) { - $this->addNodeTypeCriteria($queryBuilderCte, $filter->nodeTypes, 'pn'); + $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderCte, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), 'pn'); } return compact('queryBuilderInitial', 'queryBuilderRecursive', 'queryBuilderCte'); } @@ -770,11 +577,11 @@ private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregate $queryBuilderInitial = $this->createQueryBuilder() // @see https://mariadb.com/kb/en/library/recursive-common-table-expressions-overview/#cast-to-avoid-data-truncation ->select('n.*, h.name, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') - ->from($this->tableNamePrefix . '_node', 'n') + ->from($this->nodeQueryBuilder->contentGraphTableNames->node(), 'n') // we need to join with the hierarchy relation, because we need the node name. - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->innerJoin('n', $this->tableNamePrefix . '_node', 'p', 'p.relationanchorpoint = h.parentnodeanchor') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = p.relationanchorpoint') + ->innerJoin('n', $this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') + ->innerJoin('n', $this->nodeQueryBuilder->contentGraphTableNames->node(), 'p', 'p.relationanchorpoint = h.parentnodeanchor') + ->innerJoin('n', $this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'ph', 'ph.childnodeanchor = p.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('ph.contentstreamid = :contentStreamId') @@ -785,26 +592,21 @@ private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregate $queryBuilderRecursive = $this->createQueryBuilder() ->select('cn.*, h.name, h.subtreetags, pn.nodeaggregateid AS parentNodeAggregateId, pn.level + 1 AS level, h.position') ->from('tree', 'pn') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = pn.relationanchorpoint') - ->innerJoin('pn', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = h.childnodeanchor') + ->innerJoin('pn', $this->nodeQueryBuilder->contentGraphTableNames->hierachyRelation(), 'h', 'h.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('pn', $this->nodeQueryBuilder->contentGraphTableNames->node(), 'cn', 'cn.relationanchorpoint = h.childnodeanchor') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); $this->addSubtreeTagConstraints($queryBuilderRecursive); - $queryBuilderCte = $this->createQueryBuilder() - ->select('*') - ->from('tree', 'n') - ->setParameter('contentStreamId', $this->contentStreamId->value) - ->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); + $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint, 'tree', 'n'); if ($filter->nodeTypes !== null) { - $this->addNodeTypeCriteria($queryBuilderCte, $filter->nodeTypes); + $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderCte, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager)); } if ($filter->searchTerm !== null) { - $this->addSearchTermConstraints($queryBuilderCte, $filter->searchTerm); + $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilderCte, $filter->searchTerm); } if ($filter->propertyValue !== null) { - $this->addPropertyValueConstraints($queryBuilderCte, $filter->propertyValue); + $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilderCte, $filter->propertyValue); } return compact('queryBuilderInitial', 'queryBuilderRecursive', 'queryBuilderCte'); } @@ -817,7 +619,7 @@ private function applyOrdering(QueryBuilder $queryBuilder, Ordering $ordering, s OrderingDirection::DESCENDING => 'DESC', }; if ($orderingField->field instanceof PropertyName) { - $queryBuilder->addOrderBy($this->extractPropertyValue($orderingField->field, $nodeTableAlias), $order); + $queryBuilder->addOrderBy($this->nodeQueryBuilder->extractPropertyValue($orderingField->field, $nodeTableAlias), $order); } else { $timestampColumnName = match ($orderingField->field) { TimestampField::CREATED => 'created', @@ -832,9 +634,7 @@ private function applyOrdering(QueryBuilder $queryBuilder, Ordering $ordering, s private function applyPagination(QueryBuilder $queryBuilder, Pagination $pagination): void { - $queryBuilder - ->setMaxResults($pagination->limit) - ->setFirstResult($pagination->offset); + $queryBuilder->setMaxResults($pagination->limit)->setFirstResult($pagination->offset); } /** diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/DimensionSpacePointsRepository.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/DimensionSpacePointsRepository.php index 1a409b5abc6..374d750c3ad 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/DimensionSpacePointsRepository.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/DimensionSpacePointsRepository.php @@ -15,6 +15,7 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository; use Doctrine\DBAL\Connection; +use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames; use Neos\ContentRepository\Core\DimensionSpace\AbstractDimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; @@ -30,10 +31,13 @@ final class DimensionSpacePointsRepository */ private array $dimensionspacePointsRuntimeCache = []; + private readonly ContentGraphTableNames $contentGraphTableNames; + public function __construct( private readonly Connection $databaseConnection, - private readonly string $tableNamePrefix + string $tableNamePrefix ) { + $this->contentGraphTableNames = ContentGraphTableNames::withPrefix($tableNamePrefix); } public function insertDimensionSpacePoint(AbstractDimensionSpacePoint $dimensionSpacePoint): void @@ -81,7 +85,7 @@ public function getOriginDimensionSpacePointByHash(string $hash): OriginDimensio private function writeDimensionSpacePoint(string $hash, string $dimensionSpacePointCoordinatesJson): void { $this->databaseConnection->executeStatement( - 'INSERT IGNORE INTO ' . $this->tableNamePrefix . '_dimensionspacepoints (hash, dimensionspacepoint) VALUES (:dimensionspacepointhash, :dimensionspacepoint)', + 'INSERT IGNORE INTO ' . $this->contentGraphTableNames->dimensionSpacePoints() . ' (hash, dimensionspacepoint) VALUES (:dimensionspacepointhash, :dimensionspacepoint)', [ 'dimensionspacepointhash' => $hash, 'dimensionspacepoint' => $dimensionSpacePointCoordinatesJson @@ -96,7 +100,7 @@ private function getCoordinatesByHashFromRuntimeCache(string $hash): ?string private function fillRuntimeCacheFromDatabase(): void { - $allDimensionSpacePoints = $this->databaseConnection->fetchAllAssociative('SELECT hash, dimensionspacepoint FROM ' . $this->tableNamePrefix . '_dimensionspacepoints'); + $allDimensionSpacePoints = $this->databaseConnection->fetchAllAssociative('SELECT hash, dimensionspacepoint FROM ' . $this->contentGraphTableNames->dimensionSpacePoints()); foreach ($allDimensionSpacePoints as $dimensionSpacePointRow) { $this->dimensionspacePointsRuntimeCache[(string)$dimensionSpacePointRow['hash']] = (string)$dimensionSpacePointRow['dimensionspacepoint']; } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php new file mode 100644 index 00000000000..c04bb1c72f3 --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php @@ -0,0 +1,322 @@ +contentGraphTableNames = ContentGraphTableNames::withPrefix($tableNamePrefix); + } + + public function buildBasicNodeAggregateQuery(): QueryBuilder + { + $queryBuilder = $this->createQueryBuilder() + ->select('n.*, h.contentstreamid, h.name, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->from($this->contentGraphTableNames->node(), 'n') + ->innerJoin('n', $this->contentGraphTableNames->hierachyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') + ->innerJoin('h', $this->contentGraphTableNames->dimensionSpacePoints(), 'dsp', 'dsp.hash = h.dimensionspacepointhash') + ->where('h.contentstreamid = :contentStreamId'); + + return $queryBuilder; + } + + public function buildChildNodeAggregateQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamId $contentStreamId): QueryBuilder + { + return $this->createQueryBuilder() + ->select('cn.*, ch.name, ch.contentstreamid, ch.subtreetags, cdsp.dimensionspacepoint AS covereddimensionspacepoint') + ->from($this->contentGraphTableNames->node(), 'pn') + ->innerJoin('pn', $this->contentGraphTableNames->hierachyRelation(), 'ph', 'ph.childnodeanchor = pn.relationanchorpoint') + ->innerJoin('pn', $this->contentGraphTableNames->hierachyRelation(), 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('ch', $this->contentGraphTableNames->dimensionSpacePoints(), 'cdsp', 'cdsp.hash = ch.dimensionspacepointhash') + ->innerJoin('ch', $this->contentGraphTableNames->node(), 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') + ->where('pn.nodeaggregateid = :parentNodeAggregateId') + ->andWhere('ph.contentstreamid = :contentStreamId') + ->andWhere('ch.contentstreamid = :contentStreamId') + ->orderBy('ch.position') + ->setParameters([ + 'parentNodeAggregateId' => $parentNodeAggregateId->value, + 'contentStreamId' => $contentStreamId->value, + ]); + } + + public function buildFindRootNodeAggregatesQuery(ContentStreamId $contentStreamId, FindRootNodeAggregatesFilter $filter): QueryBuilder + { + $queryBuilder = $this->buildBasicNodeAggregateQuery() + ->andWhere('h.parentnodeanchor = :rootEdgeParentAnchorId') + ->setParameters([ + 'contentStreamId' => $contentStreamId->value, + 'rootEdgeParentAnchorId' => NodeRelationAnchorPoint::forRootEdge()->value, + ]); + + if ($filter->nodeTypeName !== null) { + $queryBuilder->andWhere('n.nodetypename = :nodeTypeName')->setParameter('nodeTypeName', $filter->nodeTypeName->value); + } + + return $queryBuilder; + } + + public function buildBasicNodeQuery(ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, string $nodeTableAlias = 'n', string $select = 'n.*, h.name, h.subtreetags'): QueryBuilder + { + return $this->createQueryBuilder() + ->select($select) + ->from($this->contentGraphTableNames->node(), $nodeTableAlias) + ->innerJoin($nodeTableAlias, $this->contentGraphTableNames->hierachyRelation(), 'h', 'h.childnodeanchor = ' . $nodeTableAlias . '.relationanchorpoint') + ->where('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $contentStreamId->value) + ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash); + } + + public function buildBasicNodeByIdQuery(NodeAggregateId $nodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder + { + return $this->buildBasicNodeQuery($contentStreamId, $dimensionSpacePoint) + ->andWhere('n.nodeaggregateid = :nodeAggregateId')->setParameter('nodeAggregateId', $nodeAggregateId->value); + } + + public function buildBasicChildNodesQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder + { + return $this->createQueryBuilder() + ->select('n.*, h.name, h.subtreetags') + ->from($this->contentGraphTableNames->node(), 'pn') + ->innerJoin('pn', $this->contentGraphTableNames->hierachyRelation(), 'h', 'h.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('pn', $this->contentGraphTableNames->node(), 'n', 'h.childnodeanchor = n.relationanchorpoint') + ->where('pn.nodeaggregateid = :parentNodeAggregateId')->setParameter('parentNodeAggregateId', $parentNodeAggregateId->value) + ->andWhere('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $contentStreamId->value) + ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash); + } + + public function buildBasicParentNodeQuery(NodeAggregateId $childNodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder + { + return $this->createQueryBuilder() + ->select('pn.*, ch.name, ch.subtreetags') + ->from($this->contentGraphTableNames->node(), 'pn') + ->innerJoin('pn', $this->contentGraphTableNames->hierachyRelation(), 'ph', 'ph.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('pn', $this->contentGraphTableNames->node(), 'cn', 'cn.relationanchorpoint = ph.childnodeanchor') + ->innerJoin('pn', $this->contentGraphTableNames->hierachyRelation(), 'ch', 'ch.childnodeanchor = pn.relationanchorpoint') + ->where('cn.nodeaggregateid = :childNodeAggregateId')->setParameter('childNodeAggregateId', $childNodeAggregateId->value) + ->andWhere('ph.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $contentStreamId->value) + ->andWhere('ch.contentstreamid = :contentStreamId') + ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash) + ->andWhere('ch.dimensionspacepointhash = :dimensionSpacePointHash'); + } + + public function buildBasicNodeSiblingsQuery(bool $preceding, NodeAggregateId $siblingNodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder + { + $sharedSubQuery = $this->createQueryBuilder() + ->from($this->contentGraphTableNames->hierachyRelation(), 'sh') + ->innerJoin('sh', $this->contentGraphTableNames->node(), 'sn', 'sn.relationanchorpoint = sh.childnodeanchor') + ->where('sn.nodeaggregateid = :siblingNodeAggregateId') + ->andWhere('sh.contentstreamid = :contentStreamId') + ->andWhere('sh.dimensionspacepointhash = :dimensionSpacePointHash'); + + $parentNodeAnchorSubQuery = (clone $sharedSubQuery)->select('sh.parentnodeanchor'); + $siblingPositionSubQuery = (clone $sharedSubQuery)->select('sh.position'); + + return $this->buildBasicNodeQuery($contentStreamId, $dimensionSpacePoint) + ->andWhere('h.parentnodeanchor = (' . $parentNodeAnchorSubQuery->getSQL() . ')') + ->andWhere('n.nodeaggregateid != :siblingNodeAggregateId')->setParameter('siblingNodeAggregateId', $siblingNodeAggregateId->value) + ->andWhere('h.position ' . ($preceding ? '<' : '>') . ' (' . $siblingPositionSubQuery->getSQL() . ')') + ->orderBy('h.position', $preceding ? 'DESC' : 'ASC'); + } + + public function buildBasicNodesCteQuery(NodeAggregateId $entryNodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, string $cteName = 'ancestry', string $cteAlias = 'pn'): QueryBuilder + { + return $this->createQueryBuilder() + ->select('*') + ->from($cteName, $cteAlias) + ->setParameter('contentStreamId', $contentStreamId->value) + ->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash) + ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); + } + + public function addNodeTypeCriteria(QueryBuilder $queryBuilder, ExpandedNodeTypeCriteria $constraintsWithSubNodeTypes, string $nodeTableAlias = 'n'): void + { + $nodeTablePrefix = $nodeTableAlias === '' ? '' : $nodeTableAlias . '.'; + $allowanceQueryPart = ''; + if (!$constraintsWithSubNodeTypes->explicitlyAllowedNodeTypeNames->isEmpty()) { + $allowanceQueryPart = $queryBuilder->expr()->in($nodeTablePrefix . 'nodetypename', ':explicitlyAllowedNodeTypeNames'); + $queryBuilder->setParameter('explicitlyAllowedNodeTypeNames', $constraintsWithSubNodeTypes->explicitlyAllowedNodeTypeNames->toStringArray(), Connection::PARAM_STR_ARRAY); + } + $denyQueryPart = ''; + if (!$constraintsWithSubNodeTypes->explicitlyDisallowedNodeTypeNames->isEmpty()) { + $denyQueryPart = $queryBuilder->expr()->notIn($nodeTablePrefix . 'nodetypename', ':explicitlyDisallowedNodeTypeNames'); + $queryBuilder->setParameter('explicitlyDisallowedNodeTypeNames', $constraintsWithSubNodeTypes->explicitlyDisallowedNodeTypeNames->toStringArray(), Connection::PARAM_STR_ARRAY); + } + if ($allowanceQueryPart && $denyQueryPart) { + if ($constraintsWithSubNodeTypes->isWildCardAllowed) { + $queryBuilder->andWhere($queryBuilder->expr()->or($allowanceQueryPart, $denyQueryPart)); + } else { + $queryBuilder->andWhere($queryBuilder->expr()->and($allowanceQueryPart, $denyQueryPart)); + } + } elseif ($allowanceQueryPart && !$constraintsWithSubNodeTypes->isWildCardAllowed) { + $queryBuilder->andWhere($allowanceQueryPart); + } elseif ($denyQueryPart) { + $queryBuilder->andWhere($denyQueryPart); + } + } + + public function addSearchTermConstraints(QueryBuilder $queryBuilder, SearchTerm $searchTerm, string $nodeTableAlias = 'n'): void + { + $queryBuilder->andWhere('JSON_SEARCH(' . $nodeTableAlias . '.properties, "one", :searchTermPattern, NULL, "$.*.value") IS NOT NULL')->setParameter('searchTermPattern', '%' . $searchTerm->term . '%'); + } + + public function addPropertyValueConstraints(QueryBuilder $queryBuilder, PropertyValueCriteriaInterface $propertyValue, string $nodeTableAlias = 'n'): void + { + $queryBuilder->andWhere($this->propertyValueConstraints($queryBuilder, $propertyValue, $nodeTableAlias)); + } + + private function propertyValueConstraints(QueryBuilder $queryBuilder, PropertyValueCriteriaInterface $propertyValue, string $nodeTableAlias): string + { + return match ($propertyValue::class) { + AndCriteria::class => (string)$queryBuilder->expr()->and($this->propertyValueConstraints($queryBuilder, $propertyValue->criteria1, $nodeTableAlias), $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria2, $nodeTableAlias)), + NegateCriteria::class => 'NOT (' . $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria, $nodeTableAlias) . ')', + OrCriteria::class => (string)$queryBuilder->expr()->or($this->propertyValueConstraints($queryBuilder, $propertyValue->criteria1, $nodeTableAlias), $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria2, $nodeTableAlias)), + PropertyValueContains::class => $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, '%' . $propertyValue->value . '%', $nodeTableAlias, $propertyValue->caseSensitive), + PropertyValueEndsWith::class => $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, '%' . $propertyValue->value, $nodeTableAlias, $propertyValue->caseSensitive), + PropertyValueEquals::class => is_string($propertyValue->value) ? $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, $nodeTableAlias, $propertyValue->caseSensitive) : $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '=', $nodeTableAlias), + PropertyValueGreaterThan::class => $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '>', $nodeTableAlias), + PropertyValueGreaterThanOrEqual::class => $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '>=', $nodeTableAlias), + PropertyValueLessThan::class => $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '<', $nodeTableAlias), + PropertyValueLessThanOrEqual::class => $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '<=', $nodeTableAlias), + PropertyValueStartsWith::class => $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value . '%', $nodeTableAlias, $propertyValue->caseSensitive), + default => throw new \InvalidArgumentException(sprintf('Invalid/unsupported property value criteria "%s"', get_debug_type($propertyValue)), 1679561062), + }; + } + + private function comparePropertyValueStatement(QueryBuilder $queryBuilder, PropertyName $propertyName, string|int|float|bool $value, string $operator, string $nodeTableAlias): string + { + $paramName = $this->createUniqueParameterName(); + $paramType = match (gettype($value)) { + 'boolean' => ParameterType::BOOLEAN, + 'integer' => ParameterType::INTEGER, + default => ParameterType::STRING, + }; + $queryBuilder->setParameter($paramName, $value, $paramType); + + return $this->extractPropertyValue($propertyName, $nodeTableAlias) . ' ' . $operator . ' :' . $paramName; + } + + public function extractPropertyValue(PropertyName $propertyName, string $nodeTableAlias): string + { + try { + $escapedPropertyName = addslashes(json_encode($propertyName->value, JSON_THROW_ON_ERROR)); + } catch (\JsonException $e) { + throw new \RuntimeException(sprintf('Failed to escape property name: %s', $e->getMessage()), 1679394579, $e); + } + + return 'JSON_EXTRACT(' . $nodeTableAlias . '.properties, \'$.' . $escapedPropertyName . '.value\')'; + } + + private function searchPropertyValueStatement(QueryBuilder $queryBuilder, PropertyName $propertyName, string|bool|int|float $value, string $nodeTableAlias, bool $caseSensitive): string + { + try { + $escapedPropertyName = addslashes(json_encode($propertyName->value, JSON_THROW_ON_ERROR)); + } catch (\JsonException $e) { + throw new \RuntimeException(sprintf('Failed to escape property name: %s', $e->getMessage()), 1679394579, $e); + } + if (is_bool($value)) { + return 'JSON_SEARCH(' . $nodeTableAlias . '.properties, \'one\', \'' . ($value ? 'true' : 'false') . '\', NULL, \'$.' . $escapedPropertyName . '.value\') IS NOT NULL'; + } + $paramName = $this->createUniqueParameterName(); + $queryBuilder->setParameter($paramName, $value); + if ($caseSensitive) { + return 'JSON_SEARCH(' . $nodeTableAlias . '.properties COLLATE utf8mb4_bin, \'one\', :' . $paramName . ' COLLATE utf8mb4_bin, NULL, \'$.' . $escapedPropertyName . '.value\') IS NOT NULL'; + } + + return 'JSON_SEARCH(' . $nodeTableAlias . '.properties, \'one\', :' . $paramName . ', NULL, \'$.' . $escapedPropertyName . '.value\') IS NOT NULL'; + } + + public function getTablenameForNode(): string + { + return $this->tableNamePrefix . '_node'; + } + + public function getTablenameForHierachyRelation(): string + { + return $this->tableNamePrefix . '_hierarchyrelation'; + } + + public function getTablenameForDimensionSpacePoints(): string + { + return $this->tableNamePrefix . '_dimensionspacepoints'; + } + + public function getTablenameForReferenceRelation(): string + { + return $this->tableNamePrefix . '_referencerelation'; + } + + /** + * @return array> + * @throws DBALException + */ + public function findUsedNodeTypeNames(): array + { + $rows = $this->fetchRows($this->createQueryBuilder() + ->select('DISTINCT nodetypename') + ->from($this->contentGraphTableNames->node())); + + return $rows; + } + + /** + * @return array> + * @throws DBALException + */ + public function fetchRows(QueryBuilder $queryBuilder): array + { + $result = $queryBuilder->execute(); + if (!$result instanceof Result) { + throw new \RuntimeException(sprintf('Failed to execute query. Expected result to be of type %s, got: %s', Result::class, get_debug_type($result)), 1701443535); + } + try { + return $result->fetchAllAssociative(); + } catch (DriverException $e) { + throw new \RuntimeException(sprintf('Failed to fetch rows from database: %s', $e->getMessage()), 1701444358, $e); + } + } + + private function createQueryBuilder(): QueryBuilder + { + return $this->connection->createQueryBuilder(); + } + + private function createUniqueParameterName(): string + { + return 'param_' . str_replace('-', '', UuidFactory::create()); + } +} diff --git a/Neos.ContentRepository.BehavioralTests/Configuration/Testing/Behat/Settings.ContentRepositoryRegistry.yaml b/Neos.ContentRepository.BehavioralTests/Configuration/Testing/Behat/Settings.ContentRepositoryRegistry.yaml index 53eafcc4977..d6e103b4c6d 100644 --- a/Neos.ContentRepository.BehavioralTests/Configuration/Testing/Behat/Settings.ContentRepositoryRegistry.yaml +++ b/Neos.ContentRepository.BehavioralTests/Configuration/Testing/Behat/Settings.ContentRepositoryRegistry.yaml @@ -10,3 +10,5 @@ Neos: factoryObjectName: 'Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\GherkinPyStringNodeBasedNodeTypeManagerFactory' contentDimensionSource: factoryObjectName: 'Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\GherkinTableNodeBasedContentDimensionSourceFactory' + contentGraphAdapterFactory: + factoryObjectName: Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphAdapterFactoryBuilder diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/01-SetNodeReferences_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/01-SetNodeReferences_ConstraintChecks.feature index c374e45a337..c1508e87c83 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/01-SetNodeReferences_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/01-SetNodeReferences_ConstraintChecks.feature @@ -77,7 +77,7 @@ Feature: Constraint checks on SetNodeReferences | sourceNodeAggregateId | "source-nodandaise" | | referenceName | "referenceProperty" | | references | [{"target":"anthony-destinode"}] | - Then the last command should have thrown an exception of type "ContentStreamDoesNotExistYet" with code 1710407870 + Then the last command should have thrown an exception of type "ContentStreamDoesNotExistYet" with code 1521386692 # checks for sourceNodeAggregateId Scenario: Try to reference nodes in a non-existent node aggregate diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 180e9fd0066..f60e4f0f696 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -29,6 +29,7 @@ use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Projection\CatchUp; use Neos\ContentRepository\Core\Projection\CatchUpOptions; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentStream\ContentStreamFinder; use Neos\ContentRepository\Core\Projection\ProjectionInterface; diff --git a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php index b23f23851c0..920bd06efc0 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php @@ -21,6 +21,8 @@ use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph; use Neos\ContentRepository\Core\EventStore\EventNormalizer; use Neos\ContentRepository\Core\EventStore\EventPersister; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterFactoryBuilderInterface; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterProvider; use Neos\ContentRepository\Core\Feature\ContentStreamCommandHandler; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\DimensionSpaceCommandHandler; use Neos\ContentRepository\Core\Feature\NodeAggregateCommandHandler; @@ -46,6 +48,8 @@ final class ContentRepositoryFactory private ProjectionFactoryDependencies $projectionFactoryDependencies; private ProjectionsAndCatchUpHooks $projectionsAndCatchUpHooks; + private ContentGraphAdapterProvider $contentGraphAdapterProvider; + public function __construct( private readonly ContentRepositoryId $contentRepositoryId, EventStoreInterface $eventStore, @@ -56,12 +60,14 @@ public function __construct( private readonly ProjectionCatchUpTriggerInterface $projectionCatchUpTrigger, private readonly UserIdProviderInterface $userIdProvider, private readonly ClockInterface $clock, + ContentGraphAdapterFactoryBuilderInterface $contentGraphAdapterFactoryBuilder, ) { $contentDimensionZookeeper = new ContentDimensionZookeeper($contentDimensionSource); $interDimensionalVariationGraph = new InterDimensionalVariationGraph( $contentDimensionSource, $contentDimensionZookeeper ); + $propertyConverter = new PropertyConverter($propertySerializer); $this->projectionFactoryDependencies = new ProjectionFactoryDependencies( $contentRepositoryId, $eventStore, @@ -73,6 +79,12 @@ public function __construct( new PropertyConverter($propertySerializer) ); $this->projectionsAndCatchUpHooks = $projectionsAndCatchUpHooksFactory->build($this->projectionFactoryDependencies); + + $this->contentGraphAdapterProvider = new ContentGraphAdapterProvider($contentGraphAdapterFactoryBuilder->build( + $this->contentRepositoryId, + $nodeTypeManager, + $propertyConverter + )); } // The following properties store "singleton" references of objects for this content repository @@ -120,11 +132,13 @@ public function getOrBuild(): ContentRepository public function buildService( ContentRepositoryServiceFactoryInterface $serviceFactory ): ContentRepositoryServiceInterface { + $serviceFactoryDependencies = ContentRepositoryServiceFactoryDependencies::create( $this->projectionFactoryDependencies, $this->getOrBuild(), $this->buildEventPersister(), $this->projectionsAndCatchUpHooks->projections, + $this->contentGraphAdapterProvider ); return $serviceFactory->build($serviceFactoryDependencies); } @@ -134,26 +148,31 @@ private function buildCommandBus(): CommandBus if (!$this->commandBus) { $this->commandBus = new CommandBus( new ContentStreamCommandHandler( + $this->contentGraphAdapterProvider ), new WorkspaceCommandHandler( $this->buildEventPersister(), $this->projectionFactoryDependencies->eventStore, $this->projectionFactoryDependencies->eventNormalizer, + $this->contentGraphAdapterProvider ), new NodeAggregateCommandHandler( $this->projectionFactoryDependencies->nodeTypeManager, $this->projectionFactoryDependencies->contentDimensionZookeeper, $this->projectionFactoryDependencies->interDimensionalVariationGraph, - $this->projectionFactoryDependencies->propertyConverter + $this->projectionFactoryDependencies->propertyConverter, + $this->contentGraphAdapterProvider ), new DimensionSpaceCommandHandler( $this->projectionFactoryDependencies->contentDimensionZookeeper, - $this->projectionFactoryDependencies->interDimensionalVariationGraph + $this->projectionFactoryDependencies->interDimensionalVariationGraph, + $this->contentGraphAdapterProvider ), new NodeDuplicationCommandHandler( $this->projectionFactoryDependencies->nodeTypeManager, $this->projectionFactoryDependencies->contentDimensionZookeeper, - $this->projectionFactoryDependencies->interDimensionalVariationGraph + $this->projectionFactoryDependencies->interDimensionalVariationGraph, + $this->contentGraphAdapterProvider ) ); } diff --git a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryServiceFactoryDependencies.php b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryServiceFactoryDependencies.php index ca9df198cee..355ce248574 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryServiceFactoryDependencies.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryServiceFactoryDependencies.php @@ -20,6 +20,7 @@ use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph; use Neos\ContentRepository\Core\EventStore\EventNormalizer; use Neos\ContentRepository\Core\EventStore\EventPersister; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterProvider; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Projection\Projections; @@ -47,6 +48,7 @@ private function __construct( // we don't need CommandBus, because this is included in ContentRepository->handle() public EventPersister $eventPersister, public Projections $projections, + public ContentGraphAdapterProvider $contentGraphAdapterProvider ) { } @@ -58,6 +60,7 @@ public static function create( ContentRepository $contentRepository, EventPersister $eventPersister, Projections $projections, + ContentGraphAdapterProvider $contentGraphAdapterProvider ): self { return new self( $projectionFactoryDependencies->contentRepositoryId, @@ -71,6 +74,7 @@ public static function create( $contentRepository, $eventPersister, $projections, + $contentGraphAdapterProvider ); } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php index b4e665910d4..89af4d32e84 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php @@ -14,11 +14,11 @@ namespace Neos\ContentRepository\Core\Feature\Common; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\Exception\DimensionSpacePointNotFound; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterInterface; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\SerializedNodeReferences; use Neos\ContentRepository\Core\Feature\NodeVariation\Exception\DimensionSpacePointIsAlreadyOccupied; @@ -49,7 +49,6 @@ use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; use Neos\ContentRepository\Core\SharedModel\Exception\PropertyCannotBeSet; use Neos\ContentRepository\Core\SharedModel\Exception\ReferenceCannotBeSet; -use Neos\ContentRepository\Core\SharedModel\Exception\RootNodeAggregateDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Exception\RootNodeAggregateTypeIsAlreadyOccupied; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; @@ -69,28 +68,30 @@ abstract protected function getNodeTypeManager(): NodeTypeManager; abstract protected function getAllowedDimensionSubspace(): DimensionSpacePointSet; + abstract protected function getContentGraphAdapter(WorkspaceName $workspaceName): ContentGraphAdapterInterface; + /** * @throws ContentStreamDoesNotExistYet */ protected function requireContentStream( - WorkspaceName $workspaceName, - ContentRepository $contentRepository + WorkspaceName $workspaceName ): ContentStreamId { - $contentStreamId = ContentStreamIdOverride::resolveContentStreamIdForWorkspace($contentRepository, $workspaceName); - if (!$contentRepository->getContentStreamFinder()->hasContentStream($contentStreamId)) { + $contentGraphAdapter = $this->getContentGraphAdapter($workspaceName); + if (!$contentGraphAdapter->hasContentStream()) { throw new ContentStreamDoesNotExistYet( - 'Content stream "' . $contentStreamId->value . '" does not exist yet.', + 'Content stream for "' . $workspaceName->value . '" does not exist yet.', 1521386692 ); } - if ($contentRepository->getContentStreamFinder()->findStateForContentStream($contentStreamId) === ContentStreamState::STATE_CLOSED) { + + if ($contentGraphAdapter->findStateForContentStream() === ContentStreamState::STATE_CLOSED) { throw new ContentStreamIsClosed( - 'Content stream "' . $contentStreamId->value . '" is closed.', + 'Content stream "' . $contentGraphAdapter->getContentStreamId()->value . '" is closed.', 1710260081 ); } - return $contentStreamId; + return $contentGraphAdapter->getContentStreamId(); } /** @@ -150,18 +151,15 @@ protected function requireNodeTypeToNotBeOfTypeRoot(NodeType $nodeType): void } protected function requireRootNodeTypeToBeUnoccupied( - NodeTypeName $nodeTypeName, - ContentStreamId $contentStreamId, - ContentRepository $contentRepository + ContentGraphAdapterInterface $contentGraphAdapter, + NodeTypeName $nodeTypeName ): void { - try { - $rootNodeAggregate = $contentRepository->getContentGraph()->findRootNodeAggregateByType( - $contentStreamId, - $nodeTypeName - ); + $rootNodeAggregateExists = $contentGraphAdapter->rootNodeAggregateWithTypeExists( + $nodeTypeName + ); + + if ($rootNodeAggregateExists) { throw RootNodeAggregateTypeIsAlreadyOccupied::butWasExpectedNotTo($nodeTypeName); - } catch (RootNodeAggregateDoesNotExist $exception) { - // all is well } } @@ -253,24 +251,22 @@ protected function requireNodeTypeToAllowNumberOfReferencesInReference(Serialize /** * NodeType and NodeName must belong together to the same node, which is the to-be-checked one. * - * @param ContentStreamId $contentStreamId + * @param ContentGraphAdapterInterface $contentGraphAdapter * @param NodeType $nodeType * @param NodeName|null $nodeName * @param array|NodeAggregateId[] $parentNodeAggregateIds * @throws NodeConstraintException */ protected function requireConstraintsImposedByAncestorsAreMet( - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, NodeType $nodeType, ?NodeName $nodeName, - array $parentNodeAggregateIds, - ContentRepository $contentRepository + array $parentNodeAggregateIds ): void { foreach ($parentNodeAggregateIds as $parentNodeAggregateId) { $parentAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $parentNodeAggregateId, - $contentRepository + $contentGraphAdapter, + $parentNodeAggregateId ); if (!$parentAggregate->classification->isTethered()) { try { @@ -283,8 +279,7 @@ protected function requireConstraintsImposedByAncestorsAreMet( } foreach ( - $contentRepository->getContentGraph()->findParentNodeAggregates( - $contentStreamId, + $contentGraphAdapter->findParentNodeAggregates( $parentNodeAggregateId ) as $grandParentNodeAggregate ) { @@ -398,12 +393,10 @@ protected function areNodeTypeConstraintsImposedByGrandparentValid( * @throws NodeAggregateCurrentlyDoesNotExist */ protected function requireProjectedNodeAggregate( - ContentStreamId $contentStreamId, - NodeAggregateId $nodeAggregateId, - ContentRepository $contentRepository + ContentGraphAdapterInterface $contentGraphAdapter, + NodeAggregateId $nodeAggregateId ): NodeAggregate { - $nodeAggregate = $contentRepository->getContentGraph()->findNodeAggregateById( - $contentStreamId, + $nodeAggregate = $contentGraphAdapter->findNodeAggregateById( $nodeAggregateId ); @@ -422,12 +415,10 @@ protected function requireProjectedNodeAggregate( * @throws NodeAggregateCurrentlyExists */ protected function requireProjectedNodeAggregateToNotExist( - ContentStreamId $contentStreamId, - NodeAggregateId $nodeAggregateId, - ContentRepository $contentRepository + ContentGraphAdapterInterface $contentGraphAdapter, + NodeAggregateId $nodeAggregateId ): void { - $nodeAggregate = $contentRepository->getContentGraph()->findNodeAggregateById( - $contentStreamId, + $nodeAggregate = $contentGraphAdapter->findNodeAggregateById( $nodeAggregateId ); @@ -443,14 +434,12 @@ protected function requireProjectedNodeAggregateToNotExist( * @throws NodeAggregateCurrentlyDoesNotExist */ public function requireProjectedParentNodeAggregate( - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, NodeAggregateId $childNodeAggregateId, - OriginDimensionSpacePoint $childOriginDimensionSpacePoint, - ContentRepository $contentRepository + OriginDimensionSpacePoint $childOriginDimensionSpacePoint ): NodeAggregate { - $parentNodeAggregate = $contentRepository->getContentGraph() + $parentNodeAggregate = $contentGraphAdapter ->findParentNodeAggregateByChildOriginDimensionSpacePoint( - $contentStreamId, $childNodeAggregateId, $childOriginDimensionSpacePoint ); @@ -459,7 +448,7 @@ public function requireProjectedParentNodeAggregate( throw new NodeAggregateCurrentlyDoesNotExist( 'Parent node aggregate for ' . $childNodeAggregateId->value . ' does currently not exist in origin dimension space point ' . $childOriginDimensionSpacePoint->toJson() - . ' and content stream ' . $contentStreamId->value, + . ' and workspace ' . $contentGraphAdapter->getWorkspaceName()->value, 1645368685 ); } @@ -478,7 +467,7 @@ protected function requireNodeAggregateToCoverDimensionSpacePoint( throw new NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint( 'Node aggregate "' . $nodeAggregate->nodeAggregateId->value . '" does currently not cover dimension space point ' - . json_encode($dimensionSpacePoint) . '.', + . json_encode($dimensionSpacePoint, JSON_THROW_ON_ERROR) . '.', 1541678877 ); } @@ -530,10 +519,9 @@ protected function requireNodeAggregateToBeUntethered(NodeAggregate $nodeAggrega * @throws NodeAggregateIsDescendant */ protected function requireNodeAggregateToNotBeDescendant( - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, NodeAggregate $nodeAggregate, - NodeAggregate $referenceNodeAggregate, - ContentRepository $contentRepository + NodeAggregate $referenceNodeAggregate ): void { if ($nodeAggregate->nodeAggregateId->equals($referenceNodeAggregate->nodeAggregateId)) { throw new NodeAggregateIsDescendant( @@ -543,16 +531,14 @@ protected function requireNodeAggregateToNotBeDescendant( ); } foreach ( - $contentRepository->getContentGraph()->findChildNodeAggregates( - $contentStreamId, + $contentGraphAdapter->findChildNodeAggregates( $referenceNodeAggregate->nodeAggregateId ) as $childReferenceNodeAggregate ) { $this->requireNodeAggregateToNotBeDescendant( - $contentStreamId, + $contentGraphAdapter, $nodeAggregate, - $childReferenceNodeAggregate, - $contentRepository + $childReferenceNodeAggregate ); } } @@ -561,19 +547,17 @@ protected function requireNodeAggregateToNotBeDescendant( * @throws NodeNameIsAlreadyOccupied */ protected function requireNodeNameToBeUnoccupied( - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, ?NodeName $nodeName, NodeAggregateId $parentNodeAggregateId, OriginDimensionSpacePoint $parentOriginDimensionSpacePoint, - DimensionSpacePointSet $dimensionSpacePoints, - ContentRepository $contentRepository + DimensionSpacePointSet $dimensionSpacePoints ): void { if ($nodeName === null) { return; } - $dimensionSpacePointsOccupiedByChildNodeName = $contentRepository->getContentGraph() + $dimensionSpacePointsOccupiedByChildNodeName = $contentGraphAdapter ->getDimensionSpacePointsOccupiedByChildNodeName( - $contentStreamId, $nodeName, $parentNodeAggregateId, $parentOriginDimensionSpacePoint, @@ -592,17 +576,16 @@ protected function requireNodeNameToBeUnoccupied( * @throws NodeNameIsAlreadyCovered */ protected function requireNodeNameToBeUncovered( - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, ?NodeName $nodeName, NodeAggregateId $parentNodeAggregateId, - DimensionSpacePointSet $dimensionSpacePointsToBeCovered, - ContentRepository $contentRepository + DimensionSpacePointSet $dimensionSpacePointsToBeCovered ): void { if ($nodeName === null) { return; } - $childNodeAggregates = $contentRepository->getContentGraph()->findChildNodeAggregatesByName( - $contentStreamId, + + $childNodeAggregates = $contentGraphAdapter->findChildNodeAggregatesByName( $parentNodeAggregateId, $nodeName ); @@ -629,7 +612,7 @@ protected function requireNodeAggregateToOccupyDimensionSpacePoint( ): void { if (!$nodeAggregate->occupiesDimensionSpacePoint($originDimensionSpacePoint)) { throw new DimensionSpacePointIsNotYetOccupied( - 'Dimension space point ' . json_encode($originDimensionSpacePoint) + 'Dimension space point ' . json_encode($originDimensionSpacePoint, JSON_THROW_ON_ERROR) . ' is not yet occupied by node aggregate "' . $nodeAggregate->nodeAggregateId->value . '"', 1552595396 ); @@ -645,7 +628,7 @@ protected function requireNodeAggregateToNotOccupyDimensionSpacePoint( ): void { if ($nodeAggregate->occupiesDimensionSpacePoint($originDimensionSpacePoint)) { throw new DimensionSpacePointIsAlreadyOccupied( - 'Dimension space point ' . json_encode($originDimensionSpacePoint) + 'Dimension space point ' . json_encode($originDimensionSpacePoint, JSON_THROW_ON_ERROR) . ' is already occupied by node aggregate "' . $nodeAggregate->nodeAggregateId->value . '"', 1552595441 ); @@ -688,13 +671,10 @@ protected function validateReferenceProperties( } protected function getExpectedVersionOfContentStream( - ContentStreamId $contentStreamId, - ContentRepository $contentRepository + ContentGraphAdapterInterface $contentGraphAdapter ): ExpectedVersion { return ExpectedVersion::fromVersion( - $contentRepository->getContentStreamFinder() - ->findVersionForContentStream($contentStreamId) - ->unwrap() + $contentGraphAdapter->findVersionForContentStream()->unwrap() ); } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/ContentStreamIdOverride.php b/Neos.ContentRepository.Core/Classes/Feature/Common/ContentStreamIdOverride.php deleted file mode 100644 index 96bf09b3a80..00000000000 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/ContentStreamIdOverride.php +++ /dev/null @@ -1,67 +0,0 @@ -getWorkspaceFinder()->findOneByName($workspaceName)?->currentContentStreamId; - - if (!$contentStreamId) { - throw new ContentStreamDoesNotExistYet( - 'Content stream for workspace "' . $workspaceName->value . '" does not exist yet.', - 1710407870 - ); - } - - return $contentStreamId; - } -} diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/NodeCreationInternals.php b/Neos.ContentRepository.Core/Classes/Feature/Common/NodeCreationInternals.php index 62c1f351f64..6c7089b6648 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/NodeCreationInternals.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/NodeCreationInternals.php @@ -14,13 +14,10 @@ namespace Neos\ContentRepository\Core\Feature\Common; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSucceedingSiblingNodesFilter; -use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterInterface; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * @internal implementation details of command handlers @@ -41,30 +38,19 @@ trait NodeCreationInternals * operates on the explicitly set succeeding sibling instead of the node itself. */ private function resolveInterdimensionalSiblingsForCreation( - ContentRepository $contentRepository, - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, NodeAggregateId $requestedSucceedingSiblingNodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, DimensionSpacePointSet $coveredDimensionSpacePoints, ): InterdimensionalSiblings { - $originSubgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, + $originAlternativeSucceedingSiblings = $contentGraphAdapter->findSucceedingSiblingNodesInSubgraph( $sourceOrigin->toDimensionSpacePoint(), - VisibilityConstraints::withoutRestrictions() - ); - $originAlternativeSucceedingSiblings = $originSubgraph->findSucceedingSiblingNodes( - $requestedSucceedingSiblingNodeAggregateId, - FindSucceedingSiblingNodesFilter::create() + $requestedSucceedingSiblingNodeAggregateId ); $interdimensionalSiblings = []; foreach ($coveredDimensionSpacePoints as $coveredDimensionSpacePoint) { - $variantSubgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, - $coveredDimensionSpacePoint, - VisibilityConstraints::withoutRestrictions() - ); - $variantSucceedingSibling = $variantSubgraph->findNodeById($requestedSucceedingSiblingNodeAggregateId); + $variantSucceedingSibling = $contentGraphAdapter->findNodeInSubgraph($coveredDimensionSpacePoint, $requestedSucceedingSiblingNodeAggregateId); if ($variantSucceedingSibling) { // a) happy path, the explicitly requested succeeding sibling also exists in this dimension space point $interdimensionalSiblings[] = new InterdimensionalSibling( @@ -76,7 +62,7 @@ private function resolveInterdimensionalSiblingsForCreation( // check the other siblings succeeding in the origin dimension space point foreach ($originAlternativeSucceedingSiblings as $originSibling) { - $alternativeVariantSucceedingSibling = $variantSubgraph->findNodeById($originSibling->nodeAggregateId); + $alternativeVariantSucceedingSibling = $contentGraphAdapter->findNodeInSubgraph($coveredDimensionSpacePoint, $originSibling->nodeAggregateId); if (!$alternativeVariantSucceedingSibling) { continue; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/NodeVariationInternals.php b/Neos.ContentRepository.Core/Classes/Feature/Common/NodeVariationInternals.php index 54b8d0a60e2..43c3b3eadc3 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/NodeVariationInternals.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/NodeVariationInternals.php @@ -14,21 +14,18 @@ namespace Neos\ContentRepository\Core\Feature\Common; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\EventStore\EventInterface; use Neos\ContentRepository\Core\EventStore\Events; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterInterface; use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeGeneralizationVariantWasCreated; use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodePeerVariantWasCreated; use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeSpecializationVariantWasCreated; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSucceedingSiblingNodesFilter; -use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; -use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** * @internal implementation details of command handlers @@ -37,12 +34,13 @@ trait NodeVariationInternals { abstract protected function getInterDimensionalVariationGraph(): DimensionSpace\InterDimensionalVariationGraph; + abstract protected function getContentGraphAdapter(WorkspaceName $workspaceName): ContentGraphAdapterInterface; + protected function createEventsForVariations( - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, - NodeAggregate $nodeAggregate, - ContentRepository $contentRepository + NodeAggregate $nodeAggregate ): Events { return match ( $this->getInterDimensionalVariationGraph()->getVariantType( @@ -51,45 +49,40 @@ protected function createEventsForVariations( ) ) { DimensionSpace\VariantType::TYPE_SPECIALIZATION => $this->handleCreateNodeSpecializationVariant( - $contentStreamId, + $contentGraphAdapter, $sourceOrigin, $targetOrigin, - $nodeAggregate, - $contentRepository + $nodeAggregate ), DimensionSpace\VariantType::TYPE_GENERALIZATION => $this->handleCreateNodeGeneralizationVariant( - $contentStreamId, + $contentGraphAdapter, $sourceOrigin, $targetOrigin, - $nodeAggregate, - $contentRepository + $nodeAggregate ), default => $this->handleCreateNodePeerVariant( - $contentStreamId, + $contentGraphAdapter, $sourceOrigin, $targetOrigin, - $nodeAggregate, - $contentRepository + $nodeAggregate ), }; } protected function handleCreateNodeSpecializationVariant( - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, - NodeAggregate $nodeAggregate, - ContentRepository $contentRepository + NodeAggregate $nodeAggregate ): Events { $specializationVisibility = $this->calculateEffectiveVisibility($targetOrigin, $nodeAggregate); $events = $this->collectNodeSpecializationVariantsThatWillHaveBeenCreated( - $contentStreamId, + $contentGraphAdapter, $sourceOrigin, $targetOrigin, $nodeAggregate, $specializationVisibility, - [], - $contentRepository + [] ); return Events::fromArray($events); @@ -100,22 +93,20 @@ protected function handleCreateNodeSpecializationVariant( * @return array */ protected function collectNodeSpecializationVariantsThatWillHaveBeenCreated( - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, NodeAggregate $nodeAggregate, DimensionSpacePointSet $specializationVisibility, - array $events, - ContentRepository $contentRepository + array $events ): array { $events[] = new NodeSpecializationVariantWasCreated( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $nodeAggregate->nodeAggregateId, $sourceOrigin, $targetOrigin, $this->resolveInterdimensionalSiblings( - $contentRepository, - $contentStreamId, + $contentGraphAdapter, $nodeAggregate->nodeAggregateId, $sourceOrigin, $specializationVisibility @@ -123,19 +114,17 @@ protected function collectNodeSpecializationVariantsThatWillHaveBeenCreated( ); foreach ( - $contentRepository->getContentGraph()->findTetheredChildNodeAggregates( - $contentStreamId, + $contentGraphAdapter->findTetheredChildNodeAggregates( $nodeAggregate->nodeAggregateId ) as $tetheredChildNodeAggregate ) { $events = $this->collectNodeSpecializationVariantsThatWillHaveBeenCreated( - $contentStreamId, + $contentGraphAdapter, $sourceOrigin, $targetOrigin, $tetheredChildNodeAggregate, $specializationVisibility, - $events, - $contentRepository + $events ); } @@ -143,21 +132,19 @@ protected function collectNodeSpecializationVariantsThatWillHaveBeenCreated( } protected function handleCreateNodeGeneralizationVariant( - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, - NodeAggregate $nodeAggregate, - ContentRepository $contentRepository + NodeAggregate $nodeAggregate ): Events { $generalizationVisibility = $this->calculateEffectiveVisibility($targetOrigin, $nodeAggregate); $events = $this->collectNodeGeneralizationVariantsThatWillHaveBeenCreated( - $contentStreamId, + $contentGraphAdapter, $sourceOrigin, $targetOrigin, $nodeAggregate, $generalizationVisibility, - [], - $contentRepository + [] ); return Events::fromArray($events); @@ -168,22 +155,20 @@ protected function handleCreateNodeGeneralizationVariant( * @return array */ protected function collectNodeGeneralizationVariantsThatWillHaveBeenCreated( - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, NodeAggregate $nodeAggregate, DimensionSpacePointSet $generalizationVisibility, - array $events, - ContentRepository $contentRepository + array $events ): array { $events[] = new NodeGeneralizationVariantWasCreated( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $nodeAggregate->nodeAggregateId, $sourceOrigin, $targetOrigin, $this->resolveInterdimensionalSiblings( - $contentRepository, - $contentStreamId, + $contentGraphAdapter, $nodeAggregate->nodeAggregateId, $sourceOrigin, $generalizationVisibility @@ -191,19 +176,17 @@ protected function collectNodeGeneralizationVariantsThatWillHaveBeenCreated( ); foreach ( - $contentRepository->getContentGraph()->findTetheredChildNodeAggregates( - $contentStreamId, + $contentGraphAdapter->findTetheredChildNodeAggregates( $nodeAggregate->nodeAggregateId ) as $tetheredChildNodeAggregate ) { $events = $this->collectNodeGeneralizationVariantsThatWillHaveBeenCreated( - $contentStreamId, + $contentGraphAdapter, $sourceOrigin, $targetOrigin, $tetheredChildNodeAggregate, $generalizationVisibility, - $events, - $contentRepository + $events ); } @@ -211,21 +194,19 @@ protected function collectNodeGeneralizationVariantsThatWillHaveBeenCreated( } protected function handleCreateNodePeerVariant( - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, - NodeAggregate $nodeAggregate, - ContentRepository $contentRepository + NodeAggregate $nodeAggregate ): Events { $peerVisibility = $this->calculateEffectiveVisibility($targetOrigin, $nodeAggregate); $events = $this->collectNodePeerVariantsThatWillHaveBeenCreated( - $contentStreamId, + $contentGraphAdapter, $sourceOrigin, $targetOrigin, $nodeAggregate, $peerVisibility, - [], - $contentRepository + [] ); return Events::fromArray($events); @@ -236,22 +217,20 @@ protected function handleCreateNodePeerVariant( * @return array */ protected function collectNodePeerVariantsThatWillHaveBeenCreated( - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, NodeAggregate $nodeAggregate, DimensionSpacePointSet $peerVisibility, - array $events, - ContentRepository $contentRepository + array $events ): array { $events[] = new NodePeerVariantWasCreated( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $nodeAggregate->nodeAggregateId, $sourceOrigin, $targetOrigin, $this->resolveInterdimensionalSiblings( - $contentRepository, - $contentStreamId, + $contentGraphAdapter, $nodeAggregate->nodeAggregateId, $sourceOrigin, $peerVisibility @@ -259,19 +238,17 @@ protected function collectNodePeerVariantsThatWillHaveBeenCreated( ); foreach ( - $contentRepository->getContentGraph()->findTetheredChildNodeAggregates( - $contentStreamId, + $contentGraphAdapter->findTetheredChildNodeAggregates( $nodeAggregate->nodeAggregateId ) as $tetheredChildNodeAggregate ) { $events = $this->collectNodePeerVariantsThatWillHaveBeenCreated( - $contentStreamId, + $contentGraphAdapter, $sourceOrigin, $targetOrigin, $tetheredChildNodeAggregate, $peerVisibility, - $events, - $contentRepository + $events ); } @@ -291,33 +268,18 @@ protected function collectNodePeerVariantsThatWillHaveBeenCreated( * except this operates on the to-be-varied node itself instead of an explicitly set succeeding sibling */ private function resolveInterdimensionalSiblings( - ContentRepository $contentRepository, - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, NodeAggregateId $varyingNodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, DimensionSpacePointSet $variantCoverage, ): InterdimensionalSiblings { - $originSubgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, - $sourceOrigin->toDimensionSpacePoint(), - VisibilityConstraints::withoutRestrictions() - ); - $originSiblings = $originSubgraph->findSucceedingSiblingNodes( - $varyingNodeAggregateId, - FindSucceedingSiblingNodesFilter::create() - ); + $originSiblings = $contentGraphAdapter->findSucceedingSiblingNodesInSubgraph($sourceOrigin->toDimensionSpacePoint(), $varyingNodeAggregateId); $interdimensionalSiblings = []; foreach ($variantCoverage as $variantDimensionSpacePoint) { - $variantSubgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, - $variantDimensionSpacePoint, - VisibilityConstraints::withoutRestrictions() - ); - // check the siblings succeeding in the origin dimension space point foreach ($originSiblings as $originSibling) { - $variantSibling = $variantSubgraph->findNodeById($originSibling->nodeAggregateId); + $variantSibling = $contentGraphAdapter->findNodeInSubgraph($variantDimensionSpacePoint, $originSibling->nodeAggregateId); if (!$variantSibling) { continue; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php b/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php index 5d2a01fce2d..7565235dbee 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php @@ -14,9 +14,9 @@ * source code. */ -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\EventStore\Events; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterInterface; use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodePeerVariantWasCreated; @@ -40,11 +40,10 @@ trait TetheredNodeInternals abstract protected function getPropertyConverter(): PropertyConverter; abstract protected function createEventsForVariations( - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, - NodeAggregate $nodeAggregate, - ContentRepository $contentRepository + NodeAggregate $nodeAggregate ): Events; /** @@ -56,15 +55,14 @@ abstract protected function createEventsForVariations( * @throws \Exception */ protected function createEventsForMissingTetheredNode( + ContentGraphAdapterInterface $contentGraphAdapter, NodeAggregate $parentNodeAggregate, OriginDimensionSpacePoint $originDimensionSpacePoint, NodeName $tetheredNodeName, ?NodeAggregateId $tetheredNodeAggregateId, - NodeType $expectedTetheredNodeType, - ContentRepository $contentRepository + NodeType $expectedTetheredNodeType ): Events { - $childNodeAggregates = $contentRepository->getContentGraph()->findChildNodeAggregatesByName( - $parentNodeAggregate->contentStreamId, + $childNodeAggregates = $contentGraphAdapter->findChildNodeAggregatesByName( $parentNodeAggregate->nodeAggregateId, $tetheredNodeName ); @@ -98,7 +96,7 @@ protected function createEventsForMissingTetheredNode( ); } else { $events[] = new NodeAggregateWithNodeWasCreated( - $parentNodeAggregate->contentStreamId, + $contentGraphAdapter->getContentStreamId(), $tetheredNodeAggregateId, $expectedTetheredNodeType->name, $rootGeneralizationOrigin, @@ -117,7 +115,7 @@ protected function createEventsForMissingTetheredNode( } else { return Events::with( new NodeAggregateWithNodeWasCreated( - $parentNodeAggregate->contentStreamId, + $contentGraphAdapter->getContentStreamId(), $tetheredNodeAggregateId ?: NodeAggregateId::create(), $expectedTetheredNodeType->name, $originDimensionSpacePoint, @@ -149,11 +147,10 @@ protected function createEventsForMissingTetheredNode( } /** @var Node $childNodeSource Node aggregates are never empty */ return $this->createEventsForVariations( - $parentNodeAggregate->contentStreamId, + $contentGraphAdapter, $childNodeSource->originDimensionSpacePoint, $originDimensionSpacePoint, - $parentNodeAggregate, - $contentRepository + $parentNodeAggregate ); } else { throw new \RuntimeException( diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentGraphAdapterFactoryBuilderInterface.php b/Neos.ContentRepository.Core/Classes/Feature/ContentGraphAdapterFactoryBuilderInterface.php new file mode 100644 index 00000000000..ae88cc3f0a6 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/ContentGraphAdapterFactoryBuilderInterface.php @@ -0,0 +1,24 @@ +.contentGraphAdapterFactory.factoryObjectName + */ +interface ContentGraphAdapterFactoryBuilderInterface +{ + public function build( + ContentRepositoryId $contentRepositoryId, + NodeTypeManager $nodeTypeManager, + PropertyConverter $propertyConverter + ): ContentGraphAdapterFactoryInterface; +} diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentGraphAdapterFactoryInterface.php b/Neos.ContentRepository.Core/Classes/Feature/ContentGraphAdapterFactoryInterface.php new file mode 100644 index 00000000000..d95d21aba26 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/ContentGraphAdapterFactoryInterface.php @@ -0,0 +1,24 @@ + + */ + public function findParentNodeAggregates( + NodeAggregateId $childNodeAggregateId + ): iterable; + + /** + * @throws NodeAggregatesTypeIsAmbiguous + */ + public function findNodeAggregateById( + NodeAggregateId $nodeAggregateId + ): ?NodeAggregate; + + public function findParentNodeAggregateByChildOriginDimensionSpacePoint( + NodeAggregateId $childNodeAggregateId, + OriginDimensionSpacePoint $childOriginDimensionSpacePoint + ): ?NodeAggregate; + + /** + * @return iterable + */ + public function findChildNodeAggregates( + NodeAggregateId $parentNodeAggregateId + ): iterable; + + /** + * @return iterable + */ + public function findTetheredChildNodeAggregates( + NodeAggregateId $parentNodeAggregateId + ): iterable; + + /** + */ + public function getDimensionSpacePointsOccupiedByChildNodeName( + NodeName $nodeName, + NodeAggregateId $parentNodeAggregateId, + OriginDimensionSpacePoint $parentNodeOriginDimensionSpacePoint, + DimensionSpacePointSet $dimensionSpacePointsToCheck + ): DimensionSpacePointSet; + + /** + * A node aggregate may have multiple child node aggregates with the same name + * as long as they do not share dimension space coverage + * + * @return iterable + */ + public function findChildNodeAggregatesByName( + NodeAggregateId $parentNodeAggregateId, + NodeName $name + ): iterable; + + /* + * NODES, basically anything you would ask a subgraph + */ + + /** + * Does the subgraph with the provided identity contain any nodes + */ + public function subgraphContainsNodes( + DimensionSpacePoint $dimensionSpacePoint + ): bool; + + /** + * Finds a specified node within a "subgraph" + */ + public function findNodeInSubgraph( + DimensionSpacePoint $coveredDimensionSpacePoint, + NodeAggregateId $nodeAggregateId + ): ?Node; + + public function findParentNodeInSubgraph( + DimensionSpacePoint $coveredDimensionSpacePoint, + NodeAggregateId $childNodeAggregateId + ): ?Node; + + public function findChildNodesInSubgraph( + DimensionSpacePoint $coveredDimensionSpacePoint, + NodeAggregateId $parentNodeAggregateId + ): Nodes; + + public function findChildNodeByNameInSubgraph( + DimensionSpacePoint $coveredDimensionSpacePoint, + NodeAggregateId $parentNodeAggregateId, + NodeName $nodeName + ): ?Node; + + public function findPreceedingSiblingNodesInSubgraph( + DimensionSpacePoint $coveredDimensionSpacePoint, + NodeAggregateId $startingSiblingNodeAggregateId + ): Nodes; + + public function findSucceedingSiblingNodesInSubgraph( + DimensionSpacePoint $coveredDimensionSpacePoint, + NodeAggregateId $startingSiblingNodeAggregateId + ): Nodes; + + /* + * CONTENT STREAMS + */ + + public function hasContentStream(): bool; + + public function findStateForContentStream(): ?ContentStreamState; + + public function findVersionForContentStream(): MaybeVersion; + + /* + * WORKSPACES + */ + + public function getWorkspace(): Workspace; +} diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentGraphAdapterProvider.php b/Neos.ContentRepository.Core/Classes/Feature/ContentGraphAdapterProvider.php new file mode 100644 index 00000000000..1de0f2abb8c --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/ContentGraphAdapterProvider.php @@ -0,0 +1,77 @@ + + */ + private array $adapterInstances = []; + + public function __construct( + public readonly ContentGraphAdapterFactoryInterface $contentGraphAdapterFactory + ) { + } + + /** + * TODO: We should not need this, + * TODO: after introducing the NodeIdentity we can change usages to + * TODO: ContentGraphAdapterProvider::fromWorkspaceName() and remove this + * + * @throws ContentStreamDoesNotExistYet if there is no content stream with the provided id + * @deprecated + * + */ + public function fromContentStreamId(ContentStreamId $contentStreamId): ContentGraphAdapterInterface + { + return $this->contentGraphAdapterFactory->createFromContentStreamId($contentStreamId); + } + + /** + * @throws WorkspaceDoesNotExist if there is no workspace with the provided name + * @throws ContentStreamDoesNotExistYet if the provided workspace does not resolve to an existing content stream + */ + public function fromWorkspaceName(WorkspaceName $workspaceName): ContentGraphAdapterInterface + { + if (isset($this->adapterInstances[$workspaceName->value])) { + return $this->adapterInstances[$workspaceName->value]; + } + + return $this->contentGraphAdapterFactory->createFromWorkspaceName($workspaceName); + } + + /** + * Stateful (dirty) override of the chosen ContentStreamId for a given workspace, it applies within the given closure. + * Implementations must ensure that requesting the contentStreamId for this workspace will resolve to the given + * override ContentStreamId and vice versa resolving the WorkspaceName from this ContentStreamId should result in the + * given WorkspaceName within the closure. + */ + public function overrideContentStreamId(WorkspaceName $workspaceName, ContentStreamId $contentStreamId, \Closure $fn): void + { + $adapter = $this->contentGraphAdapterFactory->create($workspaceName, $contentStreamId); + $replacedAdapter = $this->adapterInstances[$workspaceName->value] ?? null; + $this->adapterInstances[$workspaceName->value] = $adapter; + + try { + $fn(); + } finally { + unset($this->adapterInstances[$workspaceName->value]); + if ($replacedAdapter) { + $this->adapterInstances[$workspaceName->value] = $replacedAdapter; + } + } + } +} diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php index 86fe2b2ac22..9d7875bb61d 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php @@ -44,6 +44,11 @@ */ final class ContentStreamCommandHandler implements CommandHandlerInterface { + public function __construct( + protected readonly ContentGraphAdapterProvider $contentGraphAdapterProvider + ) { + } + public function canHandle(CommandInterface $command): bool { return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); @@ -52,11 +57,11 @@ public function canHandle(CommandInterface $command): bool public function handle(CommandInterface $command, ContentRepository $contentRepository): EventsToPublish { return match ($command::class) { - CreateContentStream::class => $this->handleCreateContentStream($command, $contentRepository), - CloseContentStream::class => $this->handleCloseContentStream($command, $contentRepository), - ReopenContentStream::class => $this->handleReopenContentStream($command, $contentRepository), - ForkContentStream::class => $this->handleForkContentStream($command, $contentRepository), - RemoveContentStream::class => $this->handleRemoveContentStream($command, $contentRepository), + CreateContentStream::class => $this->handleCreateContentStream($command), + CloseContentStream::class => $this->handleCloseContentStream($command), + ReopenContentStream::class => $this->handleReopenContentStream($command), + ForkContentStream::class => $this->handleForkContentStream($command), + RemoveContentStream::class => $this->handleRemoveContentStream($command), default => throw new \DomainException('Cannot handle commands of class ' . get_class($command), 1710408206), }; } @@ -65,10 +70,8 @@ public function handle(CommandInterface $command, ContentRepository $contentRepo * @throws ContentStreamAlreadyExists */ private function handleCreateContentStream( - CreateContentStream $command, - ContentRepository $contentRepository + CreateContentStream $command ): EventsToPublish { - $this->requireContentStreamToNotExistYet($command->contentStreamId, $contentRepository); $streamName = ContentStreamEventStreamName::fromContentStreamId($command->contentStreamId) ->getEventStreamName(); @@ -84,12 +87,11 @@ private function handleCreateContentStream( } private function handleCloseContentStream( - CloseContentStream $command, - ContentRepository $contentRepository + CloseContentStream $command ): EventsToPublish { - $this->requireContentStreamToExist($command->contentStreamId, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId, $contentRepository); - $this->requireContentStreamToNotBeClosed($command->contentStreamId, $contentRepository); + $this->requireContentStreamToExist($command->contentStreamId); + $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId); + $this->requireContentStreamToNotBeClosed($command->contentStreamId); $streamName = ContentStreamEventStreamName::fromContentStreamId($command->contentStreamId)->getEventStreamName(); return new EventsToPublish( @@ -104,12 +106,11 @@ private function handleCloseContentStream( } private function handleReopenContentStream( - ReopenContentStream $command, - ContentRepository $contentRepository + ReopenContentStream $command ): EventsToPublish { - $this->requireContentStreamToExist($command->contentStreamId, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId, $contentRepository); - $this->requireContentStreamToBeClosed($command->contentStreamId, $contentRepository); + $this->requireContentStreamToExist($command->contentStreamId); + $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId); + $this->requireContentStreamToBeClosed($command->contentStreamId); $streamName = ContentStreamEventStreamName::fromContentStreamId($command->contentStreamId)->getEventStreamName(); return new EventsToPublish( @@ -129,15 +130,14 @@ private function handleReopenContentStream( * @throws ContentStreamDoesNotExistYet */ private function handleForkContentStream( - ForkContentStream $command, - ContentRepository $contentRepository + ForkContentStream $command ): EventsToPublish { - $this->requireContentStreamToExist($command->sourceContentStreamId, $contentRepository); - $this->requireContentStreamToNotBeClosed($command->sourceContentStreamId, $contentRepository); - $this->requireContentStreamToNotExistYet($command->newContentStreamId, $contentRepository); + $this->requireContentStreamToExist($command->sourceContentStreamId); + $this->requireContentStreamToNotBeClosed($command->sourceContentStreamId); - $sourceContentStreamVersion = $contentRepository->getContentStreamFinder() - ->findVersionForContentStream($command->sourceContentStreamId); + // TOOD: THis is not great + $sourceContentStreamVersion = $this->contentGraphAdapterProvider->fromContentStreamId($command->sourceContentStreamId) + ->findVersionForContentStream(); $streamName = ContentStreamEventStreamName::fromContentStreamId($command->newContentStreamId) ->getEventStreamName(); @@ -157,11 +157,10 @@ private function handleForkContentStream( } private function handleRemoveContentStream( - RemoveContentStream $command, - ContentRepository $contentRepository + RemoveContentStream $command ): EventsToPublish { - $this->requireContentStreamToExist($command->contentStreamId, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId, $contentRepository); + $this->requireContentStreamToExist($command->contentStreamId); + $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId); $streamName = ContentStreamEventStreamName::fromContentStreamId( $command->contentStreamId @@ -178,31 +177,15 @@ private function handleRemoveContentStream( ); } - /** - * @param ContentStreamId $contentStreamId - * @throws ContentStreamAlreadyExists - */ - protected function requireContentStreamToNotExistYet( - ContentStreamId $contentStreamId, - ContentRepository $contentRepository - ): void { - if ($contentRepository->getContentStreamFinder()->hasContentStream($contentStreamId)) { - throw new ContentStreamAlreadyExists( - 'Content stream "' . $contentStreamId->value . '" already exists.', - 1521386345 - ); - } - } - /** * @param ContentStreamId $contentStreamId * @throws ContentStreamDoesNotExistYet */ protected function requireContentStreamToExist( - ContentStreamId $contentStreamId, - ContentRepository $contentRepository + ContentStreamId $contentStreamId ): void { - if (!$contentRepository->getContentStreamFinder()->hasContentStream($contentStreamId)) { + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromContentStreamId($contentStreamId); + if (!$contentGraphAdapter->hasContentStream()) { throw new ContentStreamDoesNotExistYet( 'Content stream "' . $contentStreamId->value . '" does not exist yet.', 1521386692 @@ -211,10 +194,10 @@ protected function requireContentStreamToExist( } protected function requireContentStreamToNotBeClosed( - ContentStreamId $contentStreamId, - ContentRepository $contentRepository + ContentStreamId $contentStreamId ): void { - if ($contentRepository->getContentStreamFinder()->findStateForContentStream($contentStreamId) === ContentStreamState::STATE_CLOSED) { + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromContentStreamId($contentStreamId); + if ($contentGraphAdapter->findStateForContentStream() === ContentStreamState::STATE_CLOSED) { throw new ContentStreamIsClosed( 'Content stream "' . $contentStreamId->value . '" is closed.', 1710260081 @@ -223,10 +206,10 @@ protected function requireContentStreamToNotBeClosed( } protected function requireContentStreamToBeClosed( - ContentStreamId $contentStreamId, - ContentRepository $contentRepository + ContentStreamId $contentStreamId ): void { - if ($contentRepository->getContentStreamFinder()->findStateForContentStream($contentStreamId) !== ContentStreamState::STATE_CLOSED) { + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromContentStreamId($contentStreamId); + if ($contentGraphAdapter->findStateForContentStream() !== ContentStreamState::STATE_CLOSED) { throw new ContentStreamIsNotClosed( 'Content stream "' . $contentStreamId->value . '" is not closed.', 1710405911 @@ -235,12 +218,11 @@ protected function requireContentStreamToBeClosed( } protected function getExpectedVersionOfContentStream( - ContentStreamId $contentStreamId, - ContentRepository $contentRepository + ContentStreamId $contentStreamId ): ExpectedVersion { + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromContentStreamId($contentStreamId); return ExpectedVersion::fromVersion( - $contentRepository->getContentStreamFinder() - ->findVersionForContentStream($contentStreamId) + $contentGraphAdapter->findVersionForContentStream() ->unwrap() ); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/DimensionSpaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/DimensionSpaceCommandHandler.php index ca0b66861b4..fb207e36b8e 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/DimensionSpaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/DimensionSpaceCommandHandler.php @@ -25,17 +25,14 @@ use Neos\ContentRepository\Core\DimensionSpace\VariantType; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterInterface; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterProvider; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\AddDimensionShineThrough; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Command\MoveDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Event\DimensionShineThroughWasAdded; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Event\DimensionSpacePointWasMoved; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Exception\DimensionSpacePointAlreadyExists; -use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; -use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; -use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\EventStore\Model\EventStream\ExpectedVersion; /** @@ -46,6 +43,7 @@ public function __construct( private ContentDimensionZookeeper $contentDimensionZookeeper, private InterDimensionalVariationGraph $interDimensionalVariationGraph, + private ContentGraphAdapterProvider $contentGraphAdapterProvider ) { } @@ -58,23 +56,21 @@ public function handle(CommandInterface $command, ContentRepository $contentRepo { /** @phpstan-ignore-next-line */ return match ($command::class) { - MoveDimensionSpacePoint::class => $this->handleMoveDimensionSpacePoint($command, $contentRepository), - AddDimensionShineThrough::class => $this->handleAddDimensionShineThrough($command, $contentRepository), + MoveDimensionSpacePoint::class => $this->handleMoveDimensionSpacePoint($command), + AddDimensionShineThrough::class => $this->handleAddDimensionShineThrough($command), }; } private function handleMoveDimensionSpacePoint( - MoveDimensionSpacePoint $command, - ContentRepository $contentRepository + MoveDimensionSpacePoint $command ): EventsToPublish { - $contentStreamId = $this->requireContentStreamForWorkspaceName($command->workspaceName, $contentRepository); - $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromWorkspaceName($command->workspaceName); + $streamName = ContentStreamEventStreamName::fromContentStreamId($contentGraphAdapter->getContentStreamId()) ->getEventStreamName(); self::requireDimensionSpacePointToBeEmptyInContentStream( - $command->target, - $contentStreamId, - $contentRepository->getContentGraph() + $contentGraphAdapter, + $command->target ); $this->requireDimensionSpacePointToExist($command->target); @@ -82,7 +78,7 @@ private function handleMoveDimensionSpacePoint( $streamName, Events::with( new DimensionSpacePointWasMoved( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $command->source, $command->target ), @@ -92,17 +88,15 @@ private function handleMoveDimensionSpacePoint( } private function handleAddDimensionShineThrough( - AddDimensionShineThrough $command, - ContentRepository $contentRepository + AddDimensionShineThrough $command ): EventsToPublish { - $contentStreamId = $this->requireContentStreamForWorkspaceName($command->workspaceName, $contentRepository); - $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromWorkspaceName($command->workspaceName); + $streamName = ContentStreamEventStreamName::fromContentStreamId($contentGraphAdapter->getContentStreamId()) ->getEventStreamName(); self::requireDimensionSpacePointToBeEmptyInContentStream( - $command->target, - $contentStreamId, - $contentRepository->getContentGraph() + $contentGraphAdapter, + $command->target ); $this->requireDimensionSpacePointToExist($command->target); @@ -112,7 +106,7 @@ private function handleAddDimensionShineThrough( $streamName, Events::with( new DimensionShineThroughWasAdded( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $command->source, $command->target ) @@ -133,19 +127,14 @@ protected function requireDimensionSpacePointToExist(DimensionSpacePoint $dimens } private static function requireDimensionSpacePointToBeEmptyInContentStream( - DimensionSpacePoint $dimensionSpacePoint, - ContentStreamId $contentStreamId, - ContentGraphInterface $contentGraph + ContentGraphAdapterInterface $contentGraphAdapter, + DimensionSpacePoint $dimensionSpacePoint ): void { - $subgraph = $contentGraph->getSubgraph( - $contentStreamId, - $dimensionSpacePoint, - VisibilityConstraints::withoutRestrictions() - ); - if ($subgraph->countNodes() > 0) { + $hasNodes = $contentGraphAdapter->subgraphContainsNodes($dimensionSpacePoint); + if ($hasNodes) { throw new DimensionSpacePointAlreadyExists(sprintf( 'the content stream %s already contained nodes in dimension space point %s - this is not allowed.', - $contentStreamId->value, + $contentGraphAdapter->getContentStreamId()->value, $dimensionSpacePoint->toJson(), ), 1612898126); } @@ -164,23 +153,4 @@ private function requireDimensionSpacePointToBeSpecialization( throw DimensionSpacePointIsNoSpecialization::butWasSupposedToBe($target, $source); } } - - /** - * @throws ContentStreamDoesNotExistYet - */ - protected function requireContentStreamForWorkspaceName( - WorkspaceName $workspaceName, - ContentRepository $contentRepository - ): ContentStreamId { - $contentStreamId = $contentRepository->getWorkspaceFinder()->findOneByName($workspaceName) - ?->currentContentStreamId; - if (!$contentStreamId || !$contentRepository->getContentStreamFinder()->hasContentStream($contentStreamId)) { - throw new ContentStreamDoesNotExistYet( - 'Content stream "' . $contentStreamId?->value . '" does not exist yet.', - 1521386692 - ); - } - - return $contentStreamId; - } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php index 27b49905cb6..a2c35c4bb4d 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php @@ -52,6 +52,7 @@ use Neos\ContentRepository\Core\Feature\SubtreeTagging\SubtreeTagging; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** * @internal from userland, you'll use ContentRepository::handle to dispatch commands @@ -72,41 +73,20 @@ final class NodeAggregateCommandHandler implements CommandHandlerInterface use NodeVariation; use TetheredNodeInternals; - /** - * Used for constraint checks against the current outside configuration state of node types - */ - private NodeTypeManager $nodeTypeManager; - - /** - * Used for variation resolution from the current outside state of content dimensions - */ - private DimensionSpace\InterDimensionalVariationGraph $interDimensionalVariationGraph; - - /** - * Used for constraint checks against the current outside configuration state of content dimensions - */ - private DimensionSpace\ContentDimensionZookeeper $contentDimensionZookeeper; - - protected PropertyConverter $propertyConverter; - /** * can be disabled in {@see NodeAggregateCommandHandler::withoutAncestorNodeTypeConstraintChecks()} */ private bool $ancestorNodeTypeConstraintChecksEnabled = true; public function __construct( - NodeTypeManager $nodeTypeManager, - DimensionSpace\ContentDimensionZookeeper $contentDimensionZookeeper, - DimensionSpace\InterDimensionalVariationGraph $interDimensionalVariationGraph, - PropertyConverter $propertyConverter + private readonly NodeTypeManager $nodeTypeManager, + private readonly DimensionSpace\ContentDimensionZookeeper $contentDimensionZookeeper, + private readonly DimensionSpace\InterDimensionalVariationGraph $interDimensionalVariationGraph, + private readonly PropertyConverter $propertyConverter, + protected readonly ContentGraphAdapterProvider $contentGraphAdapterProvider ) { - $this->nodeTypeManager = $nodeTypeManager; - $this->contentDimensionZookeeper = $contentDimensionZookeeper; - $this->interDimensionalVariationGraph = $interDimensionalVariationGraph; - $this->propertyConverter = $propertyConverter; } - public function canHandle(CommandInterface $command): bool { return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); @@ -116,29 +96,29 @@ public function handle(CommandInterface $command, ContentRepository $contentRepo { /** @phpstan-ignore-next-line */ return match ($command::class) { - SetNodeProperties::class => $this->handleSetNodeProperties($command, $contentRepository), + SetNodeProperties::class => $this->handleSetNodeProperties($command), SetSerializedNodeProperties::class - => $this->handleSetSerializedNodeProperties($command, $contentRepository), - SetNodeReferences::class => $this->handleSetNodeReferences($command, $contentRepository), + => $this->handleSetSerializedNodeProperties($command), + SetNodeReferences::class => $this->handleSetNodeReferences($command), SetSerializedNodeReferences::class - => $this->handleSetSerializedNodeReferences($command, $contentRepository), - ChangeNodeAggregateType::class => $this->handleChangeNodeAggregateType($command, $contentRepository), - RemoveNodeAggregate::class => $this->handleRemoveNodeAggregate($command, $contentRepository), + => $this->handleSetSerializedNodeReferences($command), + ChangeNodeAggregateType::class => $this->handleChangeNodeAggregateType($command), + RemoveNodeAggregate::class => $this->handleRemoveNodeAggregate($command), CreateNodeAggregateWithNode::class - => $this->handleCreateNodeAggregateWithNode($command, $contentRepository), + => $this->handleCreateNodeAggregateWithNode($command), CreateNodeAggregateWithNodeAndSerializedProperties::class - => $this->handleCreateNodeAggregateWithNodeAndSerializedProperties($command, $contentRepository), - MoveNodeAggregate::class => $this->handleMoveNodeAggregate($command, $contentRepository), - CreateNodeVariant::class => $this->handleCreateNodeVariant($command, $contentRepository), + => $this->handleCreateNodeAggregateWithNodeAndSerializedProperties($command), + MoveNodeAggregate::class => $this->handleMoveNodeAggregate($command), + CreateNodeVariant::class => $this->handleCreateNodeVariant($command), CreateRootNodeAggregateWithNode::class - => $this->handleCreateRootNodeAggregateWithNode($command, $contentRepository), + => $this->handleCreateRootNodeAggregateWithNode($command), UpdateRootNodeAggregateDimensions::class - => $this->handleUpdateRootNodeAggregateDimensions($command, $contentRepository), - DisableNodeAggregate::class => $this->handleDisableNodeAggregate($command, $contentRepository), - EnableNodeAggregate::class => $this->handleEnableNodeAggregate($command, $contentRepository), - TagSubtree::class => $this->handleTagSubtree($command, $contentRepository), - UntagSubtree::class => $this->handleUntagSubtree($command, $contentRepository), - ChangeNodeAggregateName::class => $this->handleChangeNodeAggregateName($command, $contentRepository), + => $this->handleUpdateRootNodeAggregateDimensions($command), + DisableNodeAggregate::class => $this->handleDisableNodeAggregate($command), + EnableNodeAggregate::class => $this->handleEnableNodeAggregate($command), + TagSubtree::class => $this->handleTagSubtree($command), + UntagSubtree::class => $this->handleUntagSubtree($command), + ChangeNodeAggregateName::class => $this->handleChangeNodeAggregateName($command), }; } @@ -167,6 +147,16 @@ public function getPropertyConverter(): PropertyConverter return $this->propertyConverter; } + /** + * @param WorkspaceName $workspaceName + * @return ContentGraphAdapterInterface + * + */ + protected function getContentGraphAdapter(WorkspaceName $workspaceName): ContentGraphAdapterInterface + { + return $this->contentGraphAdapterProvider->fromWorkspaceName($workspaceName); + } + /** * Use this closure to run code with the Ancestor Node Type Checks disabled; e.g. * during imports. diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php index b1000b3a34e..5ac5645adae 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php @@ -14,7 +14,6 @@ namespace Neos\ContentRepository\Core\Feature\NodeCreation; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\EventStore\Events; @@ -22,6 +21,7 @@ use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSiblings; use Neos\ContentRepository\Core\Feature\Common\NodeAggregateEventPublisher; use Neos\ContentRepository\Core\Feature\Common\NodeCreationInternals; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterInterface; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNodeAndSerializedProperties; @@ -42,7 +42,6 @@ use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Node\PropertyName; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * @internal implementation detail of Command Handlers @@ -68,8 +67,7 @@ abstract protected function getPropertyConverter(): PropertyConverter; abstract protected function getNodeTypeManager(): NodeTypeManager; private function handleCreateNodeAggregateWithNode( - CreateNodeAggregateWithNode $command, - ContentRepository $contentRepository + CreateNodeAggregateWithNode $command ): EventsToPublish { $this->requireNodeType($command->nodeTypeName); $this->validateProperties($command->initialPropertyValues, $command->nodeTypeName); @@ -91,7 +89,7 @@ private function handleCreateNodeAggregateWithNode( $lowLevelCommand = $lowLevelCommand->withTetheredDescendantNodeAggregateIds($command->tetheredDescendantNodeAggregateIds); } - return $this->handleCreateNodeAggregateWithNodeAndSerializedProperties($lowLevelCommand, $contentRepository); + return $this->handleCreateNodeAggregateWithNodeAndSerializedProperties($lowLevelCommand); } private function validateProperties(?PropertyValuesToWrite $propertyValues, NodeTypeName $nodeTypeName): void @@ -123,11 +121,11 @@ private function validateProperties(?PropertyValuesToWrite $propertyValues, Node * @throws NodeTypeNotFoundException */ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( - CreateNodeAggregateWithNodeAndSerializedProperties $command, - ContentRepository $contentRepository + CreateNodeAggregateWithNodeAndSerializedProperties $command ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $this->requireContentStream($command->workspaceName); + $contentGraphAdapter = $this->getContentGraphAdapter($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraphAdapter); $this->requireDimensionSpacePointToExist($command->originDimensionSpacePoint->toDimensionSpacePoint()); $nodeType = $this->requireNodeType($command->nodeTypeName); $this->requireNodeTypeToNotBeAbstract($nodeType); @@ -136,28 +134,24 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( $this->requireTetheredDescendantNodeTypesToNotBeOfTypeRoot($nodeType); if ($this->areAncestorNodeTypeConstraintChecksEnabled()) { $this->requireConstraintsImposedByAncestorsAreMet( - $contentStreamId, + $contentGraphAdapter, $nodeType, $command->nodeName, - [$command->parentNodeAggregateId], - $contentRepository + [$command->parentNodeAggregateId] ); } $this->requireProjectedNodeAggregateToNotExist( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->nodeAggregateId ); $parentNodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->parentNodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->parentNodeAggregateId ); if ($command->succeedingSiblingNodeAggregateId) { $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->succeedingSiblingNodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->succeedingSiblingNodeAggregateId ); } $this->requireNodeAggregateToCoverDimensionSpacePoint( @@ -172,12 +166,11 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( ); if ($command->nodeName) { $this->requireNodeNameToBeUnoccupied( - $contentStreamId, + $contentGraphAdapter, $command->nodeName, $command->parentNodeAggregateId, $command->originDimensionSpacePoint, - $coveredDimensionSpacePoints, - $contentRepository + $coveredDimensionSpacePoints ); } @@ -193,9 +186,8 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( $descendantNodeAggregateIds->getNodeAggregateIds() as $descendantNodeAggregateId ) { $this->requireProjectedNodeAggregateToNotExist( - $contentStreamId, - $descendantNodeAggregateId, - $contentRepository + $contentGraphAdapter, + $descendantNodeAggregateId ); } @@ -204,26 +196,25 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( $events = [ $this->createRegularWithNode( + $contentGraphAdapter, $command, - $contentStreamId, $coveredDimensionSpacePoints, - $initialPropertyValues, - $contentRepository + $initialPropertyValues ) ]; array_push($events, ...iterator_to_array($this->handleTetheredChildNodes( $command, - $contentStreamId, + $contentGraphAdapter, $nodeType, $coveredDimensionSpacePoints, $command->nodeAggregateId, $descendantNodeAggregateIds, - null, + null ))); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + ContentStreamEventStreamName::fromContentStreamId($contentGraphAdapter->getContentStreamId()) ->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand($command, Events::fromArray($events)), $expectedVersion @@ -231,21 +222,19 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( } private function createRegularWithNode( + ContentGraphAdapterInterface $contentGraphAdapter, CreateNodeAggregateWithNodeAndSerializedProperties $command, - ContentStreamId $contentStreamId, DimensionSpacePointSet $coveredDimensionSpacePoints, SerializedPropertyValues $initialPropertyValues, - ContentRepository $contentRepository, ): NodeAggregateWithNodeWasCreated { return new NodeAggregateWithNodeWasCreated( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $command->nodeAggregateId, $command->nodeTypeName, $command->originDimensionSpacePoint, $command->succeedingSiblingNodeAggregateId ? $this->resolveInterdimensionalSiblingsForCreation( - $contentRepository, - $contentStreamId, + $contentGraphAdapter, $command->succeedingSiblingNodeAggregateId, $command->originDimensionSpacePoint, $coveredDimensionSpacePoints @@ -264,12 +253,12 @@ private function createRegularWithNode( */ private function handleTetheredChildNodes( CreateNodeAggregateWithNodeAndSerializedProperties $command, - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, NodeType $nodeType, DimensionSpacePointSet $coveredDimensionSpacePoints, NodeAggregateId $parentNodeAggregateId, NodeAggregateIdsByNodePaths $nodeAggregateIds, - ?NodePath $nodePath, + ?NodePath $nodePath ): Events { $events = []; foreach ($this->getNodeTypeManager()->getTetheredNodesConfigurationForNodeType($nodeType) as $rawNodeName => $childNodeType) { @@ -283,7 +272,7 @@ private function handleTetheredChildNodes( $initialPropertyValues = SerializedPropertyValues::defaultFromNodeType($childNodeType, $this->getPropertyConverter()); $events[] = new NodeAggregateWithNodeWasCreated( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $childNodeAggregateId, $childNodeType->name, $command->originDimensionSpacePoint, @@ -296,12 +285,12 @@ private function handleTetheredChildNodes( array_push($events, ...iterator_to_array($this->handleTetheredChildNodes( $command, - $contentStreamId, + $contentGraphAdapter, $childNodeType, $coveredDimensionSpacePoints, $childNodeAggregateId, $nodeAggregateIds, - $childNodePath, + $childNodePath ))); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/NodeDisabling.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/NodeDisabling.php index eca096a52c7..b970b2157c2 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/NodeDisabling.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/NodeDisabling.php @@ -14,7 +14,6 @@ * source code. */ -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace; use Neos\ContentRepository\Core\DimensionSpace\Exception\DimensionSpacePointNotFound; use Neos\ContentRepository\Core\EventStore\Events; @@ -45,16 +44,15 @@ abstract protected function getInterDimensionalVariationGraph(): DimensionSpace\ * @throws NodeAggregatesTypeIsAmbiguous */ private function handleDisableNodeAggregate( - DisableNodeAggregate $command, - ContentRepository $contentRepository + DisableNodeAggregate $command ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $this->requireContentStream($command->workspaceName); + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromWorkspaceName($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraphAdapter); $this->requireDimensionSpacePointToExist($command->coveredDimensionSpacePoint); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->nodeAggregateId ); $this->requireNodeAggregateToCoverDimensionSpacePoint( $nodeAggregate, @@ -74,7 +72,7 @@ private function handleDisableNodeAggregate( $events = Events::with( new SubtreeWasTagged( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $command->nodeAggregateId, $affectedDimensionSpacePoints, SubtreeTag::disabled(), @@ -82,7 +80,7 @@ private function handleDisableNodeAggregate( ); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + ContentStreamEventStreamName::fromContentStreamId($contentGraphAdapter->getContentStreamId()) ->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, @@ -100,16 +98,14 @@ private function handleDisableNodeAggregate( * @throws NodeAggregatesTypeIsAmbiguous */ public function handleEnableNodeAggregate( - EnableNodeAggregate $command, - ContentRepository $contentRepository + EnableNodeAggregate $command ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $contentGraphAdapter = $this->getContentGraphAdapter($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraphAdapter); $this->requireDimensionSpacePointToExist($command->coveredDimensionSpacePoint); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->nodeAggregateId ); $this->requireNodeAggregateToCoverDimensionSpacePoint( $nodeAggregate, @@ -129,7 +125,7 @@ public function handleEnableNodeAggregate( $events = Events::with( new SubtreeWasUntagged( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $command->nodeAggregateId, $affectedDimensionSpacePoints, SubtreeTag::disabled(), @@ -137,7 +133,7 @@ public function handleEnableNodeAggregate( ); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(), + ContentStreamEventStreamName::fromContentStreamId($contentGraphAdapter->getContentStreamId())->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand($command, $events), $expectedVersion ); diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php index d88df293eab..daf97445bf8 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php @@ -14,6 +14,7 @@ namespace Neos\ContentRepository\Core\Feature\NodeDuplication; +use RuntimeException; use Neos\ContentRepository\Core\CommandHandler\CommandHandlerInterface; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\ContentRepository; @@ -27,6 +28,8 @@ use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSiblings; use Neos\ContentRepository\Core\Feature\Common\NodeAggregateEventPublisher; use Neos\ContentRepository\Core\Feature\Common\NodeCreationInternals; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterInterface; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterProvider; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated; use Neos\ContentRepository\Core\Feature\NodeDuplication\Command\CopyNodesRecursively; @@ -36,6 +39,7 @@ 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; /** * @internal from userland, you'll use ContentRepository::handle to dispatch commands @@ -49,9 +53,22 @@ public function __construct( private readonly NodeTypeManager $nodeTypeManager, private readonly ContentDimensionZookeeper $contentDimensionZookeeper, private readonly InterDimensionalVariationGraph $interDimensionalVariationGraph, + private readonly ContentGraphAdapterProvider $contentGraphAdapterProvider ) { } + /** + * WIP Should not have this signature ;) + * + * @param WorkspaceName $workspaceName + * @return ContentGraphAdapterInterface + * + */ + protected function getContentGraphAdapter(WorkspaceName $workspaceName): ContentGraphAdapterInterface + { + return $this->contentGraphAdapterProvider->fromWorkspaceName($workspaceName); + } + protected function getNodeTypeManager(): NodeTypeManager { return $this->nodeTypeManager; @@ -71,7 +88,7 @@ public function handle(CommandInterface $command, ContentRepository $contentRepo { /** @phpstan-ignore-next-line */ return match ($command::class) { - CopyNodesRecursively::class => $this->handleCopyNodesRecursively($command, $contentRepository), + CopyNodesRecursively::class => $this->handleCopyNodesRecursively($command), }; } @@ -79,12 +96,11 @@ public function handle(CommandInterface $command, ContentRepository $contentRepo * @throws NodeConstraintException */ private function handleCopyNodesRecursively( - CopyNodesRecursively $command, - ContentRepository $contentRepository + CopyNodesRecursively $command ): EventsToPublish { // Basic constraints (Content Stream / Dimension Space Point / Node Type of to-be-inserted root node) - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $contentGraphAdapter = $this->getContentGraphAdapter($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraphAdapter); $this->requireDimensionSpacePointToExist( $command->targetDimensionSpacePoint->toDimensionSpacePoint() ); @@ -95,31 +111,27 @@ private function handleCopyNodesRecursively( // NOTE: we only check this for the *root* node of the to-be-inserted structure; and not for its // children (as we want to create the structure as-is; assuming it was already valid beforehand). $this->requireConstraintsImposedByAncestorsAreMet( - $contentStreamId, + $contentGraphAdapter, $nodeType, $command->targetNodeName, - [$command->targetParentNodeAggregateId], - $contentRepository + [$command->targetParentNodeAggregateId] ); // Constraint: The new nodeAggregateIds are not allowed to exist yet. $this->requireNewNodeAggregateIdsToNotExist( - $contentStreamId, - $command->nodeAggregateIdMapping, - $contentRepository + $contentGraphAdapter, + $command->nodeAggregateIdMapping ); // Constraint: the parent node must exist in the command's DimensionSpacePoint as well $parentNodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->targetParentNodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->targetParentNodeAggregateId ); if ($command->targetSucceedingSiblingNodeAggregateId) { $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->targetSucceedingSiblingNodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->targetSucceedingSiblingNodeAggregateId ); } $this->requireNodeAggregateToCoverDimensionSpacePoint( @@ -139,19 +151,18 @@ private function handleCopyNodesRecursively( // Constraint: The node name must be free in all these dimension space points if ($command->targetNodeName) { $this->requireNodeNameToBeUnoccupied( - $contentStreamId, + $contentGraphAdapter, $command->targetNodeName, $command->targetParentNodeAggregateId, $command->targetDimensionSpacePoint, - $coveredDimensionSpacePoints, - $contentRepository + $coveredDimensionSpacePoints ); } // Now, we can start creating the recursive structure. $events = []; $this->createEventsForNodeToInsert( - $contentStreamId, + $contentGraphAdapter, $command->targetDimensionSpacePoint, $coveredDimensionSpacePoints, $command->targetParentNodeAggregateId, @@ -159,13 +170,12 @@ private function handleCopyNodesRecursively( $command->targetNodeName, $command->nodeTreeToInsert, $command->nodeAggregateIdMapping, - $contentRepository, $events ); return new EventsToPublish( ContentStreamEventStreamName::fromContentStreamId( - $contentStreamId + $contentGraphAdapter->getContentStreamId() )->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, @@ -176,15 +186,13 @@ private function handleCopyNodesRecursively( } private function requireNewNodeAggregateIdsToNotExist( - ContentStreamId $contentStreamId, - Dto\NodeAggregateIdMapping $nodeAggregateIdMapping, - ContentRepository $contentRepository + ContentGraphAdapterInterface $contentGraphAdapter, + Dto\NodeAggregateIdMapping $nodeAggregateIdMapping ): void { foreach ($nodeAggregateIdMapping->getAllNewNodeAggregateIds() as $nodeAggregateId) { $this->requireProjectedNodeAggregateToNotExist( - $contentStreamId, - $nodeAggregateId, - $contentRepository + $contentGraphAdapter, + $nodeAggregateId ); } } @@ -193,7 +201,7 @@ private function requireNewNodeAggregateIdsToNotExist( * @param array $events */ private function createEventsForNodeToInsert( - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, OriginDimensionSpacePoint $originDimensionSpacePoint, DimensionSpacePointSet $coveredDimensionSpacePoints, NodeAggregateId $targetParentNodeAggregateId, @@ -201,11 +209,10 @@ private function createEventsForNodeToInsert( ?NodeName $targetNodeName, NodeSubtreeSnapshot $nodeToInsert, Dto\NodeAggregateIdMapping $nodeAggregateIdMapping, - ContentRepository $contentRepository, array &$events, ): void { $events[] = new NodeAggregateWithNodeWasCreated( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $nodeAggregateIdMapping->getNewNodeAggregateId( $nodeToInsert->nodeAggregateId ) ?: NodeAggregateId::create(), @@ -213,8 +220,7 @@ private function createEventsForNodeToInsert( $originDimensionSpacePoint, $targetSucceedingSiblingNodeAggregateId ? $this->resolveInterdimensionalSiblingsForCreation( - $contentRepository, - $contentStreamId, + $contentGraphAdapter, $targetSucceedingSiblingNodeAggregateId, $originDimensionSpacePoint, $coveredDimensionSpacePoints @@ -228,7 +234,7 @@ private function createEventsForNodeToInsert( foreach ($nodeToInsert->childNodes as $childNodeToInsert) { $this->createEventsForNodeToInsert( - $contentStreamId, + $contentGraphAdapter, $originDimensionSpacePoint, $coveredDimensionSpacePoints, // the just-inserted node becomes the new parent node ID @@ -240,7 +246,6 @@ private function createEventsForNodeToInsert( $childNodeToInsert->nodeName, $childNodeToInsert, $nodeAggregateIdMapping, - $contentRepository, $events ); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/NodeModification.php b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/NodeModification.php index 49d71efa330..78e2e89f98a 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/NodeModification.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/NodeModification.php @@ -14,10 +14,10 @@ namespace Neos\ContentRepository\Core\Feature\NodeModification; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\Common\NodeAggregateEventPublisher; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterInterface; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetNodeProperties; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; @@ -29,7 +29,6 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\PropertyNames; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * @internal implementation detail of Command Handlers @@ -39,21 +38,19 @@ trait NodeModification abstract protected function requireNodeType(NodeTypeName $nodeTypeName): NodeType; abstract protected function requireProjectedNodeAggregate( - ContentStreamId $contentStreamId, - NodeAggregateId $nodeAggregateId, - ContentRepository $contentRepository + ContentGraphAdapterInterface $contentGraphAdapter, + NodeAggregateId $nodeAggregateId ): NodeAggregate; private function handleSetNodeProperties( - SetNodeProperties $command, - ContentRepository $contentRepository + SetNodeProperties $command ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); + $this->requireContentStream($command->workspaceName); + $contentGraphAdapter = $this->getContentGraphAdapter($command->workspaceName); $this->requireDimensionSpacePointToExist($command->originDimensionSpacePoint->toDimensionSpacePoint()); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->nodeAggregateId ); $this->requireNodeAggregateToNotBeRoot($nodeAggregate); $nodeTypeName = $nodeAggregate->nodeTypeName; @@ -71,20 +68,18 @@ private function handleSetNodeProperties( $command->propertyValues->getPropertiesToUnset() ); - return $this->handleSetSerializedNodeProperties($lowLevelCommand, $contentRepository); + return $this->handleSetSerializedNodeProperties($lowLevelCommand); } private function handleSetSerializedNodeProperties( - SetSerializedNodeProperties $command, - ContentRepository $contentRepository + SetSerializedNodeProperties $command ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $contentGraphAdapter = $this->getContentGraphAdapter($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraphAdapter); // Check if node exists $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->nodeAggregateId ); $nodeType = $this->requireNodeType($nodeAggregate->nodeTypeName); $this->requireNodeAggregateToOccupyDimensionSpacePoint($nodeAggregate, $command->originDimensionSpacePoint); @@ -98,7 +93,7 @@ private function handleSetSerializedNodeProperties( ); foreach ($affectedOrigins as $affectedOrigin) { $events[] = new NodePropertiesWereSet( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $command->nodeAggregateId, $affectedOrigin, $nodeAggregate->getCoverageByOccupant($affectedOrigin), @@ -117,7 +112,7 @@ private function handleSetSerializedNodeProperties( ); foreach ($affectedOrigins as $affectedOrigin) { $events[] = new NodePropertiesWereSet( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $command->nodeAggregateId, $affectedOrigin, $nodeAggregate->getCoverageByOccupant($affectedOrigin), @@ -130,7 +125,7 @@ private function handleSetSerializedNodeProperties( $events = $this->mergeSplitEvents($events); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + ContentStreamEventStreamName::fromContentStreamId($contentGraphAdapter->getContentStreamId()) ->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php index 9290dfdb872..7c278857240 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php @@ -14,7 +14,6 @@ namespace Neos\ContentRepository\Core\Feature\NodeMove; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\Exception\DimensionSpacePointNotFound; @@ -22,6 +21,7 @@ use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\Common\NodeAggregateEventPublisher; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterInterface; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMapping; @@ -32,20 +32,14 @@ use Neos\ContentRepository\Core\Feature\NodeMove\Dto\RelationDistributionStrategy; use Neos\ContentRepository\Core\Feature\NodeMove\Dto\SucceedingSiblingNodeMoveDestination; use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved; -use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindPrecedingSiblingNodesFilter; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSucceedingSiblingNodesFilter; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Pagination\Pagination; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; -use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateCurrentlyDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateIsDescendant; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregatesTypeIsAmbiguous; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * @internal implementation detail of Command Handlers @@ -57,9 +51,8 @@ abstract protected function getInterDimensionalVariationGraph(): DimensionSpace\ abstract protected function areAncestorNodeTypeConstraintChecksEnabled(): bool; abstract protected function requireProjectedNodeAggregate( - ContentStreamId $contentStreamId, - NodeAggregateId $nodeAggregateId, - ContentRepository $contentRepository + ContentGraphAdapterInterface $contentGraphAdapter, + NodeAggregateId $nodeAggregateId ): NodeAggregate; /** @@ -71,16 +64,15 @@ abstract protected function requireProjectedNodeAggregate( * @throws NodeAggregateIsDescendant */ private function handleMoveNodeAggregate( - MoveNodeAggregate $command, - ContentRepository $contentRepository + MoveNodeAggregate $command ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $this->requireContentStream($command->workspaceName); + $contentGraphAdapter = $this->getContentGraphAdapter($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraphAdapter); $this->requireDimensionSpacePointToExist($command->dimensionSpacePoint); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->nodeAggregateId ); $this->requireNodeAggregateToNotBeRoot($nodeAggregate); $this->requireNodeAggregateToBeUntethered($nodeAggregate); @@ -94,25 +86,22 @@ private function handleMoveNodeAggregate( if ($command->newParentNodeAggregateId) { $this->requireConstraintsImposedByAncestorsAreMet( - $contentStreamId, + $contentGraphAdapter, $this->requireNodeType($nodeAggregate->nodeTypeName), $nodeAggregate->nodeName, - [$command->newParentNodeAggregateId], - $contentRepository + [$command->newParentNodeAggregateId] ); $this->requireNodeNameToBeUncovered( - $contentStreamId, + $contentGraphAdapter, $nodeAggregate->nodeName, $command->newParentNodeAggregateId, - $affectedDimensionSpacePoints, - $contentRepository + $affectedDimensionSpacePoints ); $newParentNodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->newParentNodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->newParentNodeAggregateId ); $this->requireNodeAggregateToCoverDimensionSpacePoints( @@ -121,25 +110,22 @@ private function handleMoveNodeAggregate( ); $this->requireNodeAggregateToNotBeDescendant( - $contentStreamId, + $contentGraphAdapter, $newParentNodeAggregate, - $nodeAggregate, - $contentRepository + $nodeAggregate ); } if ($command->newPrecedingSiblingNodeAggregateId) { $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->newPrecedingSiblingNodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->newPrecedingSiblingNodeAggregateId ); } if ($command->newSucceedingSiblingNodeAggregateId) { $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->newSucceedingSiblingNodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->newSucceedingSiblingNodeAggregateId ); } @@ -149,28 +135,27 @@ private function handleMoveNodeAggregate( $originNodeMoveMappings[] = new OriginNodeMoveMapping( $movedNodeOrigin, $this->resolveCoverageNodeMoveMappings( - $contentStreamId, + $contentGraphAdapter, $nodeAggregate, $command->newParentNodeAggregateId, $command->newPrecedingSiblingNodeAggregateId, $command->newSucceedingSiblingNodeAggregateId, $movedNodeOrigin, - $affectedDimensionSpacePoints, - $contentRepository + $affectedDimensionSpacePoints ) ); } $events = Events::with( new NodeAggregateWasMoved( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $command->nodeAggregateId, OriginNodeMoveMappings::create(...$originNodeMoveMappings) ) ); $contentStreamEventStreamName = ContentStreamEventStreamName::fromContentStreamId( - $contentStreamId + $contentGraphAdapter->getContentStreamId() ); return new EventsToPublish( @@ -191,22 +176,23 @@ private function handleMoveNodeAggregate( * @todo move to content graph for more efficient calculation, if possible */ private function resolveNewParentAssignments( - /** The content stream the move operation is performed in */ - ContentStreamId $contentStreamId, + /** Adapter with workspace<>contentStream mapping the move operation is performed in */ + ContentGraphAdapterInterface $contentGraphAdapter, /** The parent node aggregate's id*/ NodeAggregateId $parentId, - DimensionSpace\DimensionSpacePoint $coveredDimensionSpacePoint, - ContentRepository $contentRepository + DimensionSpace\DimensionSpacePoint $coveredDimensionSpacePoint ): CoverageNodeMoveMapping { - $contentSubgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, + $parentNode = $contentGraphAdapter->findNodeInSubgraph( $coveredDimensionSpacePoint, - VisibilityConstraints::withoutRestrictions() + $parentId ); - $parentNode = $contentSubgraph->findNodeById($parentId); if ($parentNode === null) { throw new \InvalidArgumentException( - 'Parent ' . $parentId->value . ' not found in subgraph ' . json_encode($contentSubgraph), + sprintf( + 'Parent ' . $parentId->value . ' not found in ontentstream "%s" and dimension space point "%s" ', + $contentGraphAdapter->getContentStreamId()->value, + json_encode($coveredDimensionSpacePoint, JSON_PARTIAL_OUTPUT_ON_ERROR) + ), 1667596931 ); } @@ -236,25 +222,29 @@ private function resolveAffectedDimensionSpacePointSet( }; } - private function findSibling( - ContentSubgraphInterface $contentSubgraph, - ?NodeAggregateId $parentId, - NodeAggregateId $siblingId + private function findSiblingWithin( + ContentGraphAdapterInterface $contentGraphAdapter, + DimensionSpace\DimensionSpacePoint $coveredDimensionSpacePoint, + NodeAggregateId $siblingId, + ?NodeAggregateId $parentId ): ?Node { - $siblingCandidate = $contentSubgraph->findNodeById($siblingId); - if ($parentId && $siblingCandidate) { - // If a parent node aggregate is explicitly given, all siblings must have this parent - $parent = $contentSubgraph->findParentNode($siblingId); - if (is_null($parent)) { - throw new \InvalidArgumentException( - 'Parent ' . $parentId->value . ' not found in subgraph ' . json_encode($contentSubgraph), - 1645366837 - ); - } - if ($parent->nodeAggregateId->equals($parentId)) { - return $siblingCandidate; - } - } else { + $siblingCandidate = $contentGraphAdapter->findNodeInSubgraph($coveredDimensionSpacePoint, $siblingId); + if (!$siblingCandidate) { + return null; + } + + if (!$parentId) { + return $siblingCandidate; + } + + $parent = $contentGraphAdapter->findParentNodeInSubgraph($coveredDimensionSpacePoint, $siblingId); + if (is_null($parent)) { + throw new \InvalidArgumentException( + 'Parent ' . $parentId->value . ' not found in subgraph ' . json_encode($coveredDimensionSpacePoint, JSON_PARTIAL_OUTPUT_ON_ERROR), + 1645366837 + ); + } + if ($parent->nodeAggregateId->equals($parentId)) { return $siblingCandidate; } @@ -262,25 +252,23 @@ private function findSibling( } private function resolveSucceedingSiblingFromOriginSiblings( + ContentGraphAdapterInterface $contentGraphAdapter, NodeAggregateId $nodeAggregateId, ?NodeAggregateId $parentId, ?NodeAggregateId $precedingSiblingId, ?NodeAggregateId $succeedingSiblingId, - ContentSubgraphInterface $currentContentSubgraph, - ContentSubgraphInterface $originContentSubgraph + DimensionSpace\DimensionSpacePoint $currentDimensionSpacePoint, + DimensionSpace\DimensionSpacePoint $originDimensionSpacePoint ): ?Node { $succeedingSibling = null; $precedingSiblingCandidates = iterator_to_array( $precedingSiblingId - ? $originContentSubgraph->findPrecedingSiblingNodes($precedingSiblingId, FindPrecedingSiblingNodesFilter::create()) + ? $contentGraphAdapter->findPreceedingSiblingNodesInSubgraph($originDimensionSpacePoint, $precedingSiblingId) : Nodes::createEmpty() ); $succeedingSiblingCandidates = iterator_to_array( $succeedingSiblingId - ? $originContentSubgraph->findSucceedingSiblingNodes( - $succeedingSiblingId, - FindSucceedingSiblingNodesFilter::create() - ) + ? $contentGraphAdapter->findSucceedingSiblingNodesInSubgraph($originDimensionSpacePoint, $succeedingSiblingId) : Nodes::createEmpty() ); /* @var $precedingSiblingCandidates Node[] */ @@ -292,10 +280,11 @@ private function resolveSucceedingSiblingFromOriginSiblings( if ($succeedingSiblingCandidates[$i]->nodeAggregateId->equals($nodeAggregateId)) { \array_splice($succeedingSiblingCandidates, $i, 1); } - $succeedingSibling = $this->findSibling( - $currentContentSubgraph, - $parentId, - $succeedingSiblingCandidates[$i]->nodeAggregateId + $succeedingSibling = $this->findSiblingWithin( + $contentGraphAdapter, + $currentDimensionSpacePoint, + $succeedingSiblingCandidates[$i]->nodeAggregateId, + $parentId ); if ($succeedingSibling) { break; @@ -306,16 +295,15 @@ private function resolveSucceedingSiblingFromOriginSiblings( if ($precedingSiblingCandidates[$i]->nodeAggregateId->equals($nodeAggregateId)) { \array_splice($precedingSiblingCandidates, $i, 1); } - $precedingSibling = $this->findSibling( - $currentContentSubgraph, - $parentId, - $precedingSiblingCandidates[$i]->nodeAggregateId + $precedingSibling = $this->findSiblingWithin( + $contentGraphAdapter, + $currentDimensionSpacePoint, + $precedingSiblingCandidates[$i]->nodeAggregateId, + $parentId ); if ($precedingSibling) { - $alternateSucceedingSiblings = $currentContentSubgraph->findSucceedingSiblingNodes( - $precedingSiblingId, - FindSucceedingSiblingNodesFilter::create(pagination: Pagination::fromLimitAndOffset(1, 0)), - ); + // TODO: I don't think implementing the same filtering as for the contentGraph is sensible here, so we are fetching all siblings while only interested in the next, maybe could become a more specialised method. + $alternateSucceedingSiblings = $contentGraphAdapter->findSucceedingSiblingNodesInSubgraph($currentDimensionSpacePoint, $precedingSiblingId); if (count($alternateSucceedingSiblings) > 0) { $succeedingSibling = $alternateSucceedingSiblings->first(); break; @@ -328,8 +316,8 @@ private function resolveSucceedingSiblingFromOriginSiblings( } private function resolveCoverageNodeMoveMappings( - /** The content stream the move operation is performed in */ - ContentStreamId $contentStreamId, + /** Adapter with workspace<>contentStream mapping the move operation is performed in */ + ContentGraphAdapterInterface $contentGraphAdapter, /** The node aggregate to be moved */ NodeAggregate $nodeAggregate, /** The parent node aggregate id, has precedence over siblings when in doubt */ @@ -341,62 +329,53 @@ private function resolveCoverageNodeMoveMappings( /** A dimension space point occupied by the node aggregate to be moved */ OriginDimensionSpacePoint $originDimensionSpacePoint, /** The dimension space points affected by the move operation */ - DimensionSpacePointSet $affectedDimensionSpacePoints, - ContentRepository $contentRepository + DimensionSpacePointSet $affectedDimensionSpacePoints ): CoverageNodeMoveMappings { /** @var CoverageNodeMoveMapping[] $coverageNodeMoveMappings */ $coverageNodeMoveMappings = []; - $visibilityConstraints = VisibilityConstraints::withoutRestrictions(); - $originContentSubgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, - $originDimensionSpacePoint->toDimensionSpacePoint(), - $visibilityConstraints - ); foreach ( $nodeAggregate->getCoverageByOccupant($originDimensionSpacePoint) ->getIntersection($affectedDimensionSpacePoints) as $dimensionSpacePoint ) { - $contentSubgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, - $dimensionSpacePoint, - $visibilityConstraints - ); - $succeedingSibling = $succeedingSiblingId - ? $this->findSibling($contentSubgraph, $parentId, $succeedingSiblingId) + ? $this->findSiblingWithin($contentGraphAdapter, $dimensionSpacePoint, $succeedingSiblingId, $parentId) : null; if (!$succeedingSibling) { $precedingSibling = $precedingSiblingId - ? $this->findSibling($contentSubgraph, $parentId, $precedingSiblingId) + ? $this->findSiblingWithin($contentGraphAdapter, $dimensionSpacePoint, $precedingSiblingId, $parentId) : null; if ($precedingSiblingId && $precedingSibling) { - $alternateSucceedingSiblings = $contentSubgraph->findSucceedingSiblingNodes( - $precedingSiblingId, - FindSucceedingSiblingNodesFilter::create(pagination: Pagination::fromLimitAndOffset(1, 0)), + $alternateSucceedingSiblings = $contentGraphAdapter->findSucceedingSiblingNodesInSubgraph( + $dimensionSpacePoint, + $precedingSiblingId ); if (count($alternateSucceedingSiblings) > 0) { $succeedingSibling = $alternateSucceedingSiblings->first(); } } else { $succeedingSibling = $this->resolveSucceedingSiblingFromOriginSiblings( + $contentGraphAdapter, $nodeAggregate->nodeAggregateId, $parentId, $precedingSiblingId, $succeedingSiblingId, - $contentSubgraph, - $originContentSubgraph + $dimensionSpacePoint, + $originDimensionSpacePoint->toDimensionSpacePoint() ); } } if ($succeedingSibling) { // for the event payload, we additionally need the parent of the succeeding sibling - $parentOfSucceedingSibling = $contentSubgraph->findParentNode($succeedingSibling->nodeAggregateId); + $parentOfSucceedingSibling = $contentGraphAdapter->findParentNodeInSubgraph($dimensionSpacePoint, $succeedingSibling->nodeAggregateId); if ($parentOfSucceedingSibling === null) { throw new \InvalidArgumentException( - 'Parent of succeeding sibling ' . $succeedingSibling->nodeAggregateId->value - . ' not found in subgraph ' . json_encode($contentSubgraph), + sprintf( + 'Parent of succeeding sibling ' . $succeedingSibling->nodeAggregateId->value . ' not found in contentstream "%s" and dimension space point "%s" ', + $contentGraphAdapter->getContentStreamId()->value, + json_encode($dimensionSpacePoint, JSON_PARTIAL_OUTPUT_ON_ERROR) + ), 1667817639 ); } @@ -417,19 +396,22 @@ private function resolveCoverageNodeMoveMappings( if ($parentId === null) { // if parent ID is not given, use the parent of the original node, because we want to move // to the end of the sibling list. - $parentId = $contentSubgraph->findParentNode($nodeAggregate->nodeAggregateId)?->nodeAggregateId; + $parentId = $contentGraphAdapter->findParentNodeInSubgraph($dimensionSpacePoint, $nodeAggregate->nodeAggregateId)?->nodeAggregateId; if ($parentId === null) { throw new \InvalidArgumentException( - 'Parent ' . $parentId . ' not found in subgraph ' . json_encode($contentSubgraph), + sprintf( + 'Parent ' . $parentId . ' not found in contentstream "%s" and dimension space point "%s" ', + $contentGraphAdapter->getContentStreamId()->value, + json_encode($dimensionSpacePoint, JSON_PARTIAL_OUTPUT_ON_ERROR) + ), 1667597013 ); } } $coverageNodeMoveMappings[] = $this->resolveNewParentAssignments( - $contentStreamId, + $contentGraphAdapter, $parentId, - $dimensionSpacePoint, - $contentRepository + $dimensionSpacePoint ); } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/NodeReferencing.php b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/NodeReferencing.php index 507e5c6bb6c..f91da9114c6 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/NodeReferencing.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/NodeReferencing.php @@ -14,11 +14,11 @@ namespace Neos\ContentRepository\Core\Feature\NodeReferencing; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\Common\ConstraintChecks; use Neos\ContentRepository\Core\Feature\Common\NodeAggregateEventPublisher; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterInterface; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyScope; use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetNodeReferences; @@ -28,7 +28,6 @@ use Neos\ContentRepository\Core\Feature\NodeReferencing\Event\NodeReferencesWereSet; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * @internal implementation detail of Command Handlers @@ -38,22 +37,20 @@ trait NodeReferencing use ConstraintChecks; abstract protected function requireProjectedNodeAggregate( - ContentStreamId $contentStreamId, - NodeAggregateId $nodeAggregateId, - ContentRepository $contentRepository + ContentGraphAdapterInterface $contentGraphAdapter, + NodeAggregateId $nodeAggregateId ): NodeAggregate; private function handleSetNodeReferences( - SetNodeReferences $command, - ContentRepository $contentRepository + SetNodeReferences $command ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); + $this->requireContentStream($command->workspaceName); + $contentGraphAdapter = $this->getContentGraphAdapter($command->workspaceName); $this->requireDimensionSpacePointToExist($command->sourceOriginDimensionSpacePoint->toDimensionSpacePoint()); $sourceNodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->sourceNodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->sourceNodeAggregateId ); $this->requireNodeAggregateToNotBeRoot($sourceNodeAggregate); $nodeTypeName = $sourceNodeAggregate->nodeTypeName; @@ -88,25 +85,23 @@ private function handleSetNodeReferences( )), ); - return $this->handleSetSerializedNodeReferences($lowLevelCommand, $contentRepository); + return $this->handleSetSerializedNodeReferences($lowLevelCommand); } /** * @throws \Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet */ private function handleSetSerializedNodeReferences( - SetSerializedNodeReferences $command, - ContentRepository $contentRepository + SetSerializedNodeReferences $command ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $contentGraphAdapter = $this->getContentGraphAdapter($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraphAdapter); $this->requireDimensionSpacePointToExist( $command->sourceOriginDimensionSpacePoint->toDimensionSpacePoint() ); $sourceNodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->sourceNodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->sourceNodeAggregateId ); $this->requireNodeAggregateToNotBeRoot($sourceNodeAggregate); $this->requireNodeAggregateToOccupyDimensionSpacePoint( @@ -124,9 +119,8 @@ private function handleSetSerializedNodeReferences( foreach ($command->references as $reference) { assert($reference instanceof SerializedNodeReference); $destinationNodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $reference->targetNodeAggregateId, - $contentRepository + $contentGraphAdapter, + $reference->targetNodeAggregateId ); $this->requireNodeAggregateToNotBeRoot($destinationNodeAggregate); $this->requireNodeAggregateToCoverDimensionSpacePoint( @@ -152,7 +146,7 @@ private function handleSetSerializedNodeReferences( $events = Events::with( new NodeReferencesWereSet( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $command->sourceNodeAggregateId, $affectedOrigins, $command->referenceName, @@ -161,7 +155,7 @@ private function handleSetSerializedNodeReferences( ); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + ContentStreamEventStreamName::fromContentStreamId($contentGraphAdapter->getContentStreamId()) ->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php index f169088ff73..085af255b8c 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php @@ -21,7 +21,6 @@ use Neos\ContentRepository\Core\Feature\WorkspacePublication\Dto\NodeIdToPublishOrDiscard; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeVariantSelectionStrategy; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/NodeRemoval.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/NodeRemoval.php index 446f7b58e45..91f4da561a8 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/NodeRemoval.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/NodeRemoval.php @@ -14,7 +14,6 @@ namespace Neos\ContentRepository\Core\Feature\NodeRemoval; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace; use Neos\ContentRepository\Core\DimensionSpace\Exception\DimensionSpacePointNotFound; use Neos\ContentRepository\Core\EventStore\Events; @@ -46,15 +45,14 @@ abstract protected function areAncestorNodeTypeConstraintChecksEnabled(): bool; * @throws DimensionSpacePointNotFound */ private function handleRemoveNodeAggregate( - RemoveNodeAggregate $command, - ContentRepository $contentRepository + RemoveNodeAggregate $command ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $this->requireContentStream($command->workspaceName); + $contentGraphAdapter = $this->getContentGraphAdapter($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraphAdapter); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->nodeAggregateId ); $this->requireDimensionSpacePointToExist($command->coveredDimensionSpacePoint); $this->requireNodeAggregateNotToBeTethered($nodeAggregate); @@ -64,15 +62,14 @@ private function handleRemoveNodeAggregate( ); if ($command->removalAttachmentPoint instanceof NodeAggregateId) { $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->removalAttachmentPoint, - $contentRepository + $contentGraphAdapter, + $command->removalAttachmentPoint ); } $events = Events::with( new NodeAggregateWasRemoved( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $command->nodeAggregateId, $command->nodeVariantSelectionStrategy->resolveAffectedOriginDimensionSpacePoints( $nodeAggregate->getOccupationByCovered($command->coveredDimensionSpacePoint), @@ -89,7 +86,7 @@ private function handleRemoveNodeAggregate( ); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + ContentStreamEventStreamName::fromContentStreamId($contentGraphAdapter->getContentStreamId()) ->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php index 40403f523ba..d3f259c26eb 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php @@ -30,40 +30,38 @@ trait NodeRenaming { use ConstraintChecks; - private function handleChangeNodeAggregateName(ChangeNodeAggregateName $command, ContentRepository $contentRepository): EventsToPublish + private function handleChangeNodeAggregateName(ChangeNodeAggregateName $command): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $contentGraphAdapter = $this->getContentGraphAdapter($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraphAdapter); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->nodeAggregateId ); $this->requireNodeAggregateToNotBeRoot($nodeAggregate, 'and Root Node Aggregates cannot be renamed'); $this->requireNodeAggregateToBeUntethered($nodeAggregate); - foreach ($contentRepository->getContentGraph()->findParentNodeAggregates($contentStreamId, $command->nodeAggregateId) as $parentNodeAggregate) { + foreach ($contentGraphAdapter->findParentNodeAggregates($command->nodeAggregateId) as $parentNodeAggregate) { foreach ($parentNodeAggregate->occupiedDimensionSpacePoints as $occupiedParentDimensionSpacePoint) { $this->requireNodeNameToBeUnoccupied( - $contentStreamId, + $contentGraphAdapter, $command->newNodeName, $parentNodeAggregate->nodeAggregateId, $occupiedParentDimensionSpacePoint, - $parentNodeAggregate->coveredDimensionSpacePoints, - $contentRepository + $parentNodeAggregate->coveredDimensionSpacePoints ); } } $events = Events::with( new NodeAggregateNameWasChanged( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $command->nodeAggregateId, $command->newNodeName, ), ); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(), + ContentStreamEventStreamName::fromContentStreamId($contentGraphAdapter->getContentStreamId())->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, $events diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php index 25e60f27449..afdccb7791e 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php @@ -14,13 +14,13 @@ namespace Neos\ContentRepository\Core\Feature\NodeTypeChange; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePointSet; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\Common\NodeAggregateEventPublisher; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterInterface; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\NodeRemoval\Event\NodeAggregateWasRemoved; use Neos\ContentRepository\Core\Feature\NodeTypeChange\Command\ChangeNodeAggregateType; @@ -31,14 +31,12 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; -use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregatesTypeIsAmbiguous; use Neos\ContentRepository\Core\SharedModel\Exception\NodeConstraintException; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFound; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** @codingStandardsIgnoreStart */ /** @codingStandardsIgnoreEnd */ @@ -51,17 +49,15 @@ trait NodeTypeChange abstract protected function getNodeTypeManager(): NodeTypeManager; abstract protected function requireProjectedNodeAggregate( - ContentStreamId $contentStreamId, - NodeAggregateId $nodeAggregateId, - ContentRepository $contentRepository + ContentGraphAdapterInterface $contentRepositoryAdapter, + NodeAggregateId $nodeAggregateId ): NodeAggregate; abstract protected function requireConstraintsImposedByAncestorsAreMet( - ContentStreamId $contentStreamId, + ContentGraphAdapterInterface $contentGraphAdapter, NodeType $nodeType, ?NodeName $nodeName, - array $parentNodeAggregateIds, - ContentRepository $contentRepository + array $parentNodeAggregateIds ): void; abstract protected function requireNodeTypeConstraintsImposedByParentToBeMet( @@ -89,12 +85,12 @@ abstract protected function areNodeTypeConstraintsImposedByGrandparentValid( ): bool; abstract protected function createEventsForMissingTetheredNode( + ContentGraphAdapterInterface $contentGraphAdapter, NodeAggregate $parentNodeAggregate, OriginDimensionSpacePoint $originDimensionSpacePoint, NodeName $tetheredNodeName, NodeAggregateId $tetheredNodeAggregateId, - NodeType $expectedTetheredNodeType, - ContentRepository $contentRepository + NodeType $expectedTetheredNodeType ): Events; /** @@ -102,22 +98,21 @@ abstract protected function createEventsForMissingTetheredNode( * @throws NodeConstraintException * @throws NodeTypeNotFoundException * @throws NodeAggregatesTypeIsAmbiguous + * @throws \Exception */ private function handleChangeNodeAggregateType( - ChangeNodeAggregateType $command, - ContentRepository $contentRepository + ChangeNodeAggregateType $command ): EventsToPublish { /************** * Constraint checks **************/ // existence of content stream, node type and node aggregate - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $contentGraphAdapter = $this->getContentGraphAdapter($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraphAdapter); $newNodeType = $this->requireNodeType($command->newNodeTypeName); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->nodeAggregateId ); // node type detail checks @@ -126,25 +121,23 @@ private function handleChangeNodeAggregateType( $this->requireTetheredDescendantNodeTypesToNotBeOfTypeRoot($newNodeType); // the new node type must be allowed at this position in the tree - $parentNodeAggregates = $contentRepository->getContentGraph()->findParentNodeAggregates( - $nodeAggregate->contentStreamId, + $parentNodeAggregates = $contentGraphAdapter->findParentNodeAggregates( $nodeAggregate->nodeAggregateId ); foreach ($parentNodeAggregates as $parentNodeAggregate) { assert($parentNodeAggregate instanceof NodeAggregate); $this->requireConstraintsImposedByAncestorsAreMet( - $contentStreamId, + $contentGraphAdapter, $newNodeType, $nodeAggregate->nodeName, - [$parentNodeAggregate->nodeAggregateId], - $contentRepository + [$parentNodeAggregate->nodeAggregateId] ); } /** @codingStandardsIgnoreStart */ match ($command->strategy) { NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy::STRATEGY_HAPPY_PATH - => $this->requireConstraintsImposedByHappyPathStrategyAreMet($nodeAggregate, $newNodeType, $contentRepository), + => $this->requireConstraintsImposedByHappyPathStrategyAreMet($contentGraphAdapter, $nodeAggregate, $newNodeType), NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy::STRATEGY_DELETE => null }; /** @codingStandardsIgnoreStop */ @@ -165,7 +158,7 @@ private function handleChangeNodeAggregateType( **************/ $events = [ new NodeAggregateTypeWasChanged( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $command->nodeAggregateId, $command->newNodeTypeName ), @@ -174,14 +167,14 @@ private function handleChangeNodeAggregateType( // remove disallowed nodes if ($command->strategy === NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy::STRATEGY_DELETE) { array_push($events, ...iterator_to_array($this->deleteDisallowedNodesWhenChangingNodeType( + $contentGraphAdapter, $nodeAggregate, - $newNodeType, - $contentRepository + $newNodeType ))); array_push($events, ...iterator_to_array($this->deleteObsoleteTetheredNodesWhenChangingNodeType( + $contentGraphAdapter, $nodeAggregate, - $newNodeType, - $contentRepository + $newNodeType ))); } @@ -192,33 +185,30 @@ private function handleChangeNodeAggregateType( foreach ($expectedTetheredNodes as $serializedTetheredNodeName => $expectedTetheredNodeType) { $tetheredNodeName = NodeName::fromString($serializedTetheredNodeName); - $subgraph = $contentRepository->getContentGraph()->getSubgraph( - $node->subgraphIdentity->contentStreamId, + $tetheredNode = $contentGraphAdapter->findChildNodeByNameInSubgraph( $node->originDimensionSpacePoint->toDimensionSpacePoint(), - VisibilityConstraints::withoutRestrictions() - ); - $tetheredNode = $subgraph->findNodeByPath( - $tetheredNodeName, - $node->nodeAggregateId + $node->nodeAggregateId, + $tetheredNodeName ); + if ($tetheredNode === null) { $tetheredNodeAggregateId = $command->tetheredDescendantNodeAggregateIds ->getNodeAggregateId(NodePath::fromString($tetheredNodeName->value)) ?: NodeAggregateId::create(); array_push($events, ...iterator_to_array($this->createEventsForMissingTetheredNode( + $contentGraphAdapter, $nodeAggregate, $node->originDimensionSpacePoint, $tetheredNodeName, $tetheredNodeAggregateId, - $expectedTetheredNodeType, - $contentRepository + $expectedTetheredNodeType ))); } } } return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(), + ContentStreamEventStreamName::fromContentStreamId($contentGraphAdapter->getContentStreamId())->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, Events::fromArray($events), @@ -234,14 +224,13 @@ private function handleChangeNodeAggregateType( * @throws NodeConstraintException|NodeTypeNotFoundException */ private function requireConstraintsImposedByHappyPathStrategyAreMet( + ContentGraphAdapterInterface $contentGraphAdapter, NodeAggregate $nodeAggregate, - NodeType $newNodeType, - ContentRepository $contentRepository + NodeType $newNodeType ): void { // if we have children, we need to check whether they are still allowed // after we changed the node type of the $nodeAggregate to $newNodeType. - $childNodeAggregates = $contentRepository->getContentGraph()->findChildNodeAggregates( - $nodeAggregate->contentStreamId, + $childNodeAggregates = $contentGraphAdapter->findChildNodeAggregates( $nodeAggregate->nodeAggregateId ); foreach ($childNodeAggregates as $childNodeAggregate) { @@ -256,11 +245,9 @@ private function requireConstraintsImposedByHappyPathStrategyAreMet( // we do not need to test for grandparents here, as we did not modify the grandparents. // Thus, if it was allowed before, it is allowed now. - // additionally, we need to look one level down to the grandchildren as well // - as it could happen that these are affected by our constraint checks as well. - $grandchildNodeAggregates = $contentRepository->getContentGraph()->findChildNodeAggregates( - $childNodeAggregate->contentStreamId, + $grandchildNodeAggregates = $contentGraphAdapter->findChildNodeAggregates( $childNodeAggregate->nodeAggregateId ); foreach ($grandchildNodeAggregates as $grandchildNodeAggregate) { @@ -282,15 +269,14 @@ private function requireConstraintsImposedByHappyPathStrategyAreMet( * needs to be modified as well (as they are structurally the same) */ private function deleteDisallowedNodesWhenChangingNodeType( + ContentGraphAdapterInterface $contentGraphAdapter, NodeAggregate $nodeAggregate, - NodeType $newNodeType, - ContentRepository $contentRepository + NodeType $newNodeType ): Events { $events = []; // if we have children, we need to check whether they are still allowed // after we changed the node type of the $nodeAggregate to $newNodeType. - $childNodeAggregates = $contentRepository->getContentGraph()->findChildNodeAggregates( - $nodeAggregate->contentStreamId, + $childNodeAggregates = $contentGraphAdapter->findChildNodeAggregates( $nodeAggregate->nodeAggregateId ); foreach ($childNodeAggregates as $childNodeAggregate) { @@ -308,9 +294,9 @@ private function deleteDisallowedNodesWhenChangingNodeType( // this aggregate (or parts thereof) are DISALLOWED according to constraints. // We now need to find out which edges we need to remove, $dimensionSpacePointsToBeRemoved = $this->findDimensionSpacePointsConnectingParentAndChildAggregate( + $contentGraphAdapter, $nodeAggregate, - $childNodeAggregate, - $contentRepository + $childNodeAggregate ); // AND REMOVE THEM $events[] = $this->removeNodeInDimensionSpacePointSet( @@ -321,13 +307,9 @@ private function deleteDisallowedNodesWhenChangingNodeType( // we do not need to test for grandparents here, as we did not modify the grandparents. // Thus, if it was allowed before, it is allowed now. - // additionally, we need to look one level down to the grandchildren as well // - as it could happen that these are affected by our constraint checks as well. - $grandchildNodeAggregates = $contentRepository->getContentGraph()->findChildNodeAggregates( - $childNodeAggregate->contentStreamId, - $childNodeAggregate->nodeAggregateId - ); + $grandchildNodeAggregates = $contentGraphAdapter->findChildNodeAggregates($childNodeAggregate->nodeAggregateId); foreach ($grandchildNodeAggregates as $grandchildNodeAggregate) { /* @var $grandchildNodeAggregate NodeAggregate */ // we do not need to test for the parent of grandchild (=child), @@ -344,9 +326,9 @@ private function deleteDisallowedNodesWhenChangingNodeType( // this aggregate (or parts thereof) are DISALLOWED according to constraints. // We now need to find out which edges we need to remove, $dimensionSpacePointsToBeRemoved = $this->findDimensionSpacePointsConnectingParentAndChildAggregate( + $contentGraphAdapter, $childNodeAggregate, - $grandchildNodeAggregate, - $contentRepository + $grandchildNodeAggregate ); // AND REMOVE THEM $events[] = $this->removeNodeInDimensionSpacePointSet( @@ -361,18 +343,15 @@ private function deleteDisallowedNodesWhenChangingNodeType( } private function deleteObsoleteTetheredNodesWhenChangingNodeType( + ContentGraphAdapterInterface $contentGraphAdapter, NodeAggregate $nodeAggregate, - NodeType $newNodeType, - ContentRepository $contentRepository + NodeType $newNodeType ): Events { $expectedTetheredNodes = $this->getNodeTypeManager()->getTetheredNodesConfigurationForNodeType($newNodeType); $events = []; // find disallowed tethered nodes - $tetheredNodeAggregates = $contentRepository->getContentGraph()->findTetheredChildNodeAggregates( - $nodeAggregate->contentStreamId, - $nodeAggregate->nodeAggregateId - ); + $tetheredNodeAggregates = $contentGraphAdapter->findTetheredChildNodeAggregates($nodeAggregate->nodeAggregateId); foreach ($tetheredNodeAggregates as $tetheredNodeAggregate) { /* @var $tetheredNodeAggregate NodeAggregate */ @@ -380,9 +359,9 @@ private function deleteObsoleteTetheredNodesWhenChangingNodeType( // this aggregate (or parts thereof) are DISALLOWED according to constraints. // We now need to find out which edges we need to remove, $dimensionSpacePointsToBeRemoved = $this->findDimensionSpacePointsConnectingParentAndChildAggregate( + $contentGraphAdapter, $nodeAggregate, - $tetheredNodeAggregate, - $contentRepository + $tetheredNodeAggregate ); // AND REMOVE THEM $events[] = $this->removeNodeInDimensionSpacePointSet( @@ -419,18 +398,16 @@ private function deleteObsoleteTetheredNodesWhenChangingNodeType( * we originated from) */ private function findDimensionSpacePointsConnectingParentAndChildAggregate( + ContentGraphAdapterInterface $contentGraphAdapter, NodeAggregate $parentNodeAggregate, - NodeAggregate $childNodeAggregate, - ContentRepository $contentRepository + NodeAggregate $childNodeAggregate ): DimensionSpacePointSet { $points = []; foreach ($childNodeAggregate->coveredDimensionSpacePoints as $coveredDimensionSpacePoint) { - $subgraph = $contentRepository->getContentGraph()->getSubgraph( - $childNodeAggregate->contentStreamId, + $parentNode = $contentGraphAdapter->findParentNodeInSubgraph( $coveredDimensionSpacePoint, - VisibilityConstraints::withoutRestrictions() + $childNodeAggregate->nodeAggregateId ); - $parentNode = $subgraph->findParentNode($childNodeAggregate->nodeAggregateId); if ( $parentNode && $parentNode->nodeAggregateId->equals($parentNodeAggregate->nodeAggregateId) diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php index c842644e845..001f9bd66e9 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php @@ -20,7 +20,6 @@ use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Dto\NodeIdToPublishOrDiscard; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/NodeVariation.php b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/NodeVariation.php index 7df4a3acb5b..19db8561142 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/NodeVariation.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/NodeVariation.php @@ -14,7 +14,6 @@ namespace Neos\ContentRepository\Core\Feature\NodeVariation; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\Exception\DimensionSpacePointNotFound; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\Common\ConstraintChecks; @@ -47,15 +46,14 @@ trait NodeVariation * @throws NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint */ private function handleCreateNodeVariant( - CreateNodeVariant $command, - ContentRepository $contentRepository + CreateNodeVariant $command ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $this->requireContentStream($command->workspaceName); + $contentGraphAdapter = $this->getContentGraphAdapter($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraphAdapter); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->nodeAggregateId ); // we do this check first, because it gives a more meaningful error message on what you need to do. // we cannot use sentences with "." because the UI will only print the 1st sentence :/ @@ -66,10 +64,9 @@ private function handleCreateNodeVariant( $this->requireNodeAggregateToOccupyDimensionSpacePoint($nodeAggregate, $command->sourceOrigin); $this->requireNodeAggregateToNotOccupyDimensionSpacePoint($nodeAggregate, $command->targetOrigin); $parentNodeAggregate = $this->requireProjectedParentNodeAggregate( - $contentStreamId, + $contentGraphAdapter, $command->nodeAggregateId, - $command->sourceOrigin, - $contentRepository + $command->sourceOrigin ); $this->requireNodeAggregateToCoverDimensionSpacePoint( $parentNodeAggregate, @@ -77,15 +74,14 @@ private function handleCreateNodeVariant( ); $events = $this->createEventsForVariations( - $contentStreamId, + $contentGraphAdapter, $command->sourceOrigin, $command->targetOrigin, - $nodeAggregate, - $contentRepository + $nodeAggregate ); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(), + ContentStreamEventStreamName::fromContentStreamId($contentGraphAdapter->getContentStreamId())->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, $events diff --git a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/RootNodeHandling.php b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/RootNodeHandling.php index 0fb102fe6ef..1b23712a19e 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/RootNodeHandling.php +++ b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/RootNodeHandling.php @@ -14,7 +14,6 @@ namespace Neos\ContentRepository\Core\Feature\RootNodeCreation; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\EventStore\Events; @@ -43,6 +42,7 @@ 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; /** * @internal implementation detail of Command Handlers @@ -67,23 +67,21 @@ abstract protected function requireNodeTypeToBeOfTypeRoot(NodeType $nodeType): v * @throws NodeTypeIsNotOfTypeRoot */ private function handleCreateRootNodeAggregateWithNode( - CreateRootNodeAggregateWithNode $command, - ContentRepository $contentRepository + CreateRootNodeAggregateWithNode $command ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $this->requireContentStream($command->workspaceName); + $contentGraphAdapter = $this->getContentGraphAdapter($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraphAdapter); $this->requireProjectedNodeAggregateToNotExist( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->nodeAggregateId ); $nodeType = $this->requireNodeType($command->nodeTypeName); $this->requireNodeTypeToNotBeAbstract($nodeType); $this->requireNodeTypeToBeOfTypeRoot($nodeType); $this->requireRootNodeTypeToBeUnoccupied( - $nodeType->name, - $contentStreamId, - $contentRepository + $contentGraphAdapter, + $nodeType->name ); $descendantNodeAggregateIds = $command->tetheredDescendantNodeAggregateIds->completeForNodeOfType( @@ -97,25 +95,24 @@ private function handleCreateRootNodeAggregateWithNode( $events = [ $this->createRootWithNode( $command, - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $this->getAllowedDimensionSubspace() ) ]; foreach ($this->getInterDimensionalVariationGraph()->getRootGeneralizations() as $rootGeneralization) { array_push($events, ...iterator_to_array($this->handleTetheredRootChildNodes( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $nodeType, OriginDimensionSpacePoint::fromDimensionSpacePoint($rootGeneralization), $this->getInterDimensionalVariationGraph()->getSpecializationSet($rootGeneralization, true), $command->nodeAggregateId, $command->tetheredDescendantNodeAggregateIds, - null, - $contentRepository + null ))); } - $contentStreamEventStream = ContentStreamEventStreamName::fromContentStreamId($contentStreamId); + $contentStreamEventStream = ContentStreamEventStreamName::fromContentStreamId($contentGraphAdapter->getContentStreamId()); return new EventsToPublish( $contentStreamEventStream->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( @@ -145,15 +142,13 @@ private function createRootWithNode( * @return EventsToPublish */ private function handleUpdateRootNodeAggregateDimensions( - UpdateRootNodeAggregateDimensions $command, - ContentRepository $contentRepository + UpdateRootNodeAggregateDimensions $command ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $contentGraphAdapter = $this->getContentGraphAdapter($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraphAdapter); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->nodeAggregateId ); if (!$nodeAggregate->classification->isRoot()) { throw new NodeAggregateIsNotRoot('The node aggregate ' . $nodeAggregate->nodeAggregateId->value . ' is not classified as root, but should be for command UpdateRootNodeAggregateDimensions.', 1678647355); @@ -161,14 +156,14 @@ private function handleUpdateRootNodeAggregateDimensions( $events = Events::with( new RootNodeAggregateDimensionsWereUpdated( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $command->nodeAggregateId, $this->getAllowedDimensionSubspace() ) ); $contentStreamEventStream = ContentStreamEventStreamName::fromContentStreamId( - $contentStreamId + $contentGraphAdapter->getContentStreamId() ); return new EventsToPublish( $contentStreamEventStream->getEventStreamName(), @@ -191,8 +186,7 @@ private function handleTetheredRootChildNodes( DimensionSpacePointSet $coveredDimensionSpacePoints, NodeAggregateId $parentNodeAggregateId, NodeAggregateIdsByNodePaths $nodeAggregateIdsByNodePath, - ?NodePath $nodePath, - ContentRepository $contentRepository, + ?NodePath $nodePath ): Events { $events = []; foreach ($this->getNodeTypeManager()->getTetheredNodesConfigurationForNodeType($nodeType) as $rawNodeName => $childNodeType) { @@ -223,8 +217,7 @@ private function handleTetheredRootChildNodes( $coveredDimensionSpacePoints, $childNodeAggregateId, $nodeAggregateIdsByNodePath, - $childNodePath, - $contentRepository + $childNodePath ))); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/SubtreeTagging.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/SubtreeTagging.php index 05f3c58248e..12ed6be6f6d 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/SubtreeTagging.php +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/SubtreeTagging.php @@ -14,7 +14,6 @@ * source code. */ -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; @@ -36,11 +35,11 @@ trait SubtreeTagging abstract protected function getInterDimensionalVariationGraph(): DimensionSpace\InterDimensionalVariationGraph; - private function handleTagSubtree(TagSubtree $command, ContentRepository $contentRepository): EventsToPublish + private function handleTagSubtree(TagSubtree $command): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); + $contentGraphAdapter = $this->getContentGraphAdapter($command->workspaceName); $this->requireDimensionSpacePointToExist($command->coveredDimensionSpacePoint); - $nodeAggregate = $this->requireProjectedNodeAggregate($contentStreamId, $command->nodeAggregateId, $contentRepository); + $nodeAggregate = $this->requireProjectedNodeAggregate($contentGraphAdapter, $command->nodeAggregateId); $this->requireNodeAggregateToCoverDimensionSpacePoint( $nodeAggregate, $command->coveredDimensionSpacePoint @@ -60,7 +59,7 @@ private function handleTagSubtree(TagSubtree $command, ContentRepository $conten $events = Events::with( new SubtreeWasTagged( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $command->nodeAggregateId, $affectedDimensionSpacePoints, $command->tag, @@ -68,7 +67,7 @@ private function handleTagSubtree(TagSubtree $command, ContentRepository $conten ); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + ContentStreamEventStreamName::fromContentStreamId($contentGraphAdapter->getContentStreamId()) ->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, @@ -78,14 +77,13 @@ private function handleTagSubtree(TagSubtree $command, ContentRepository $conten ); } - public function handleUntagSubtree(UntagSubtree $command, ContentRepository $contentRepository): EventsToPublish + public function handleUntagSubtree(UntagSubtree $command): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); + $contentGraphAdapter = $this->getContentGraphAdapter($command->workspaceName); $this->requireDimensionSpacePointToExist($command->coveredDimensionSpacePoint); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraphAdapter, + $command->nodeAggregateId ); $this->requireNodeAggregateToCoverDimensionSpacePoint( $nodeAggregate, @@ -106,7 +104,7 @@ public function handleUntagSubtree(UntagSubtree $command, ContentRepository $con $events = Events::with( new SubtreeWasUntagged( - $contentStreamId, + $contentGraphAdapter->getContentStreamId(), $command->nodeAggregateId, $affectedDimensionSpacePoints, $command->tag, @@ -114,7 +112,7 @@ public function handleUntagSubtree(UntagSubtree $command, ContentRepository $con ); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(), + ContentStreamEventStreamName::fromContentStreamId($contentGraphAdapter->getContentStreamId())->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand($command, $events), ExpectedVersion::ANY() ); diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index f03b3a20633..303d0c21aa1 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -24,7 +24,6 @@ use Neos\ContentRepository\Core\EventStore\EventPersister; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; -use Neos\ContentRepository\Core\Feature\Common\ContentStreamIdOverride; use Neos\ContentRepository\Core\Feature\Common\MatchableWithNodeIdToPublishOrDiscardInterface; use Neos\ContentRepository\Core\Feature\Common\PublishableToOtherContentStreamsInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; @@ -89,6 +88,7 @@ public function __construct( private EventPersister $eventPersister, private EventStoreInterface $eventStore, private EventNormalizer $eventNormalizer, + private ContentGraphAdapterProvider $contentGraphAdapterProvider ) { } @@ -102,7 +102,7 @@ public function handle(CommandInterface $command, ContentRepository $contentRepo /** @phpstan-ignore-next-line */ return match ($command::class) { CreateWorkspace::class => $this->handleCreateWorkspace($command, $contentRepository), - RenameWorkspace::class => $this->handleRenameWorkspace($command, $contentRepository), + RenameWorkspace::class => $this->handleRenameWorkspace($command), CreateRootWorkspace::class => $this->handleCreateRootWorkspace($command, $contentRepository), PublishWorkspace::class => $this->handlePublishWorkspace($command, $contentRepository), RebaseWorkspace::class => $this->handleRebaseWorkspace($command, $contentRepository), @@ -110,7 +110,7 @@ public function handle(CommandInterface $command, ContentRepository $contentRepo DiscardIndividualNodesFromWorkspace::class => $this->handleDiscardIndividualNodesFromWorkspace($command, $contentRepository), DiscardWorkspace::class => $this->handleDiscardWorkspace($command, $contentRepository), DeleteWorkspace::class => $this->handleDeleteWorkspace($command, $contentRepository), - ChangeWorkspaceOwner::class => $this->handleChangeWorkspaceOwner($command, $contentRepository), + ChangeWorkspaceOwner::class => $this->handleChangeWorkspaceOwner($command), ChangeBaseWorkspace::class => $this->handleChangeBaseWorkspace($command, $contentRepository), }; } @@ -125,28 +125,33 @@ private function handleCreateWorkspace( CreateWorkspace $command, ContentRepository $contentRepository, ): EventsToPublish { - $existingWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName($command->workspaceName); - if ($existingWorkspace !== null) { - throw new WorkspaceAlreadyExists(sprintf( - 'The workspace %s already exists', - $command->workspaceName->value - ), 1505830958921); + try { + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromWorkspaceName($command->workspaceName); + $contentStreamId = $contentGraphAdapter->getContentStreamId(); + } catch (ContentStreamDoesNotExistYet $e) { + // Desired outcome } - $baseWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName($command->baseWorkspaceName); - if ($baseWorkspace === null) { - throw new BaseWorkspaceDoesNotExist(sprintf( - 'The workspace %s (base workspace of %s) does not exist', - $command->baseWorkspaceName->value, - $command->workspaceName->value - ), 1513890708); - } + isset($contentStreamId) + && throw new WorkspaceAlreadyExists(sprintf( + 'The workspace %s already exists', + $command->workspaceName->value + ), 1505830958921); + + $baseWorkspaceContentGraphAdapter = $this->contentGraphAdapterProvider->fromWorkspaceName($command->baseWorkspaceName); +// if ($baseWorkspace === null) { +// throw new BaseWorkspaceDoesNotExist(sprintf( +// 'The workspace %s (base workspace of %s) does not exist', +// $command->baseWorkspaceName->value, +// $command->workspaceName->value +// ), 1513890708); +// } // When the workspace is created, we first have to fork the content stream $contentRepository->handle( ForkContentStream::create( $command->newContentStreamId, - $baseWorkspace->currentContentStreamId, + $baseWorkspaceContentGraphAdapter->getContentStreamId(), ) )->block(); @@ -171,9 +176,9 @@ private function handleCreateWorkspace( /** * @throws WorkspaceDoesNotExist */ - private function handleRenameWorkspace(RenameWorkspace $command, ContentRepository $contentRepository): EventsToPublish + private function handleRenameWorkspace(RenameWorkspace $command): EventsToPublish { - $this->requireWorkspace($command->workspaceName, $contentRepository); + $this->requireWorkspace($command->workspaceName); $events = Events::with( new WorkspaceWasRenamed( @@ -200,14 +205,19 @@ private function handleCreateRootWorkspace( CreateRootWorkspace $command, ContentRepository $contentRepository, ): EventsToPublish { - $existingWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName($command->workspaceName); - if ($existingWorkspace !== null) { - throw new WorkspaceAlreadyExists(sprintf( - 'The workspace %s already exists', - $command->workspaceName->value - ), 1505848624450); + try { + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromWorkspaceName($command->workspaceName); + $contentStreamId = $contentGraphAdapter->getContentStreamId(); + } catch (ContentStreamDoesNotExistYet $e) { + // Desired outcome } + isset($contentStreamId) + && throw new WorkspaceAlreadyExists(sprintf( + 'The workspace %s already exists', + $command->workspaceName->value + ), 1505848624450); + $newContentStreamId = $command->newContentStreamId; $contentRepository->handle( CreateContentStream::create( @@ -244,8 +254,8 @@ private function handlePublishWorkspace( PublishWorkspace $command, ContentRepository $contentRepository, ): EventsToPublish { - $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); - $baseWorkspace = $this->requireBaseWorkspace($workspace, $contentRepository); + $workspace = $this->requireWorkspace($command->workspaceName); + $baseWorkspace = $this->requireBaseWorkspace($workspace); $this->publishContentStream( $workspace->currentContentStreamId, @@ -358,8 +368,8 @@ private function handleRebaseWorkspace( RebaseWorkspace $command, ContentRepository $contentRepository, ): EventsToPublish { - $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); - $baseWorkspace = $this->requireBaseWorkspace($workspace, $contentRepository); + $workspace = $this->requireWorkspace($command->workspaceName); + $baseWorkspace = $this->requireBaseWorkspace($workspace); $oldWorkspaceContentStreamId = $workspace->currentContentStreamId; $oldWorkspaceContentStreamIdState = $contentRepository->getContentStreamFinder() ->findStateForContentStream($oldWorkspaceContentStreamId); @@ -389,25 +399,23 @@ private function handleRebaseWorkspace( // 2) extract the commands from the to-be-rebased content stream; and applies them on the new content stream $originalCommands = $this->extractCommandsFromContentStreamMetadata($workspaceContentStreamName); $commandsThatFailed = new CommandsThatFailedDuringRebase(); - ContentStreamIdOverride::applyContentStreamIdToClosure( - $command->rebasedContentStreamId, - function () use ($originalCommands, $contentRepository, &$commandsThatFailed): void { - foreach ($originalCommands as $sequenceNumber => $originalCommand) { - // We no longer need to adjust commands as the workspace stays the same - try { - $contentRepository->handle($originalCommand)->block(); - } catch (\Exception $e) { - $commandsThatFailed = $commandsThatFailed->add( - new CommandThatFailedDuringRebase( - $sequenceNumber, - $originalCommand, - $e - ) - ); - } + $this->contentGraphAdapterProvider->overrideContentStreamId($command->workspaceName, $command->rebasedContentStreamId, function () use ($originalCommands, $contentRepository, &$commandsThatFailed): void { + foreach ($originalCommands as $sequenceNumber => $originalCommand) { + // We no longer need to adjust commands as the workspace stays the same + try { + $contentRepository->handle($originalCommand)->block(); + // if we came this far, we know the command was applied successfully. + } catch (\Exception $e) { + $commandsThatFailed = $commandsThatFailed->add( + new CommandThatFailedDuringRebase( + $sequenceNumber, + $originalCommand, + $e + ) + ); } } - ); + }); // 3) if we got so far without an exception (or if we don't care), we can switch the workspace's active content stream. if ($command->rebaseErrorHandlingStrategy === RebaseErrorHandlingStrategy::STRATEGY_FORCE || $commandsThatFailed->isEmpty()) { @@ -490,17 +498,18 @@ private function handlePublishIndividualNodesFromWorkspace( PublishIndividualNodesFromWorkspace $command, ContentRepository $contentRepository, ): EventsToPublish { - $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromWorkspaceName($command->workspaceName); + $workspace = $this->requireWorkspace($command->workspaceName); $oldWorkspaceContentStreamId = $workspace->currentContentStreamId; - $oldWorkspaceContentStreamIdState = $contentRepository->getContentStreamFinder()->findStateForContentStream($oldWorkspaceContentStreamId); + $oldWorkspaceContentStreamIdState = $contentGraphAdapter->findStateForContentStream(); if ($oldWorkspaceContentStreamIdState === null) { throw new \DomainException('Cannot publish nodes on a workspace with a stateless content stream', 1710410114); } - $baseWorkspace = $this->requireBaseWorkspace($workspace, $contentRepository); + $baseWorkspace = $this->requireBaseWorkspace($workspace); // 1) close old content stream $contentRepository->handle( - CloseContentStream::create($oldWorkspaceContentStreamId) + CloseContentStream::create($contentGraphAdapter->getContentStreamId()) ); // 2) separate commands in two parts - the ones MATCHING the nodes from the command, and the REST @@ -521,7 +530,8 @@ private function handlePublishIndividualNodesFromWorkspace( try { // 4) using the new content stream, apply the matching commands - ContentStreamIdOverride::applyContentStreamIdToClosure( + $this->contentGraphAdapterProvider->overrideContentStreamId( + $baseWorkspace->workspaceName, $command->contentStreamIdForMatchingPart, function () use ($matchingCommands, $contentRepository, $baseWorkspace): void { foreach ($matchingCommands as $matchingCommand) { @@ -554,7 +564,8 @@ function () use ($matchingCommands, $contentRepository, $baseWorkspace): void { )->block(); // 7) apply REMAINING commands to the workspace's new content stream - ContentStreamIdOverride::applyContentStreamIdToClosure( + $this->contentGraphAdapterProvider->overrideContentStreamId( + $command->workspaceName, $command->contentStreamIdForRemainingPart, function () use ($contentRepository, $remainingCommands) { foreach ($remainingCommands as $remainingCommand) { @@ -626,13 +637,14 @@ private function handleDiscardIndividualNodesFromWorkspace( DiscardIndividualNodesFromWorkspace $command, ContentRepository $contentRepository, ): EventsToPublish { - $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); - $oldWorkspaceContentStreamId = $workspace->currentContentStreamId; - $oldWorkspaceContentStreamIdState = $contentRepository->getContentStreamFinder()->findStateForContentStream($oldWorkspaceContentStreamId); + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromWorkspaceName($command->workspaceName); + $workspace = $contentGraphAdapter->getWorkspace(); + $oldWorkspaceContentStreamId = $contentGraphAdapter->getContentStreamId(); + $oldWorkspaceContentStreamIdState = $contentGraphAdapter->findStateForContentStream(); if ($oldWorkspaceContentStreamIdState === null) { throw new \DomainException('Cannot discard nodes on a workspace with a stateless content stream', 1710408112); } - $baseWorkspace = $this->requireBaseWorkspace($workspace, $contentRepository); + $baseWorkspace = $this->requireBaseWorkspace($workspace); // 1) close old content stream $contentRepository->handle( @@ -657,7 +669,8 @@ private function handleDiscardIndividualNodesFromWorkspace( // 4) using the new content stream, apply the commands to keep try { - ContentStreamIdOverride::applyContentStreamIdToClosure( + $this->contentGraphAdapterProvider->overrideContentStreamId( + $baseWorkspace->workspaceName, $command->newContentStreamId, function () use ($commandsToKeep, $contentRepository, $baseWorkspace): void { foreach ($commandsToKeep as $matchingCommand) { @@ -769,8 +782,8 @@ private function handleDiscardWorkspace( DiscardWorkspace $command, ContentRepository $contentRepository, ): EventsToPublish { - $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); - $baseWorkspace = $this->requireBaseWorkspace($workspace, $contentRepository); + $workspace = $this->requireWorkspace($command->workspaceName); + $baseWorkspace = $this->requireBaseWorkspace($workspace); $newContentStream = $command->newContentStreamId; $contentRepository->handle( @@ -811,13 +824,13 @@ private function handleChangeBaseWorkspace( ChangeBaseWorkspace $command, ContentRepository $contentRepository, ): EventsToPublish { - $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); + $workspace = $this->requireWorkspace($command->workspaceName); $this->requireEmptyWorkspace($workspace); - $this->requireBaseWorkspace($workspace, $contentRepository); + $this->requireBaseWorkspace($workspace); - $baseWorkspace = $this->requireWorkspace($command->baseWorkspaceName, $contentRepository); + $baseWorkspace = $this->requireWorkspace($command->baseWorkspaceName); - $this->requireNonCircularRelationBetweenWorkspaces($workspace, $baseWorkspace, $contentRepository); + $this->requireNonCircularRelationBetweenWorkspaces($workspace, $baseWorkspace); $contentRepository->handle( ForkContentStream::create( @@ -849,7 +862,7 @@ private function handleDeleteWorkspace( DeleteWorkspace $command, ContentRepository $contentRepository, ): EventsToPublish { - $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); + $workspace = $this->requireWorkspace($command->workspaceName); $contentRepository->handle( RemoveContentStream::create( @@ -875,10 +888,9 @@ private function handleDeleteWorkspace( * @throws WorkspaceDoesNotExist */ private function handleChangeWorkspaceOwner( - ChangeWorkspaceOwner $command, - ContentRepository $contentRepository, + ChangeWorkspaceOwner $command ): EventsToPublish { - $this->requireWorkspace($command->workspaceName, $contentRepository); + $this->requireWorkspace($command->workspaceName); $events = Events::with( new WorkspaceOwnerWasChanged( @@ -898,28 +910,26 @@ private function handleChangeWorkspaceOwner( /** * @throws WorkspaceDoesNotExist */ - private function requireWorkspace(WorkspaceName $workspaceName, ContentRepository $contentRepository): Workspace + private function requireWorkspace(WorkspaceName $workspaceName): Workspace { - $workspace = $contentRepository->getWorkspaceFinder()->findOneByName($workspaceName); - if (is_null($workspace)) { - throw WorkspaceDoesNotExist::butWasSupposedTo($workspaceName); - } - - return $workspace; + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromWorkspaceName($workspaceName); + return $contentGraphAdapter->getWorkspace(); } /** * @throws WorkspaceHasNoBaseWorkspaceName * @throws BaseWorkspaceDoesNotExist */ - private function requireBaseWorkspace(Workspace $workspace, ContentRepository $contentRepository): Workspace + private function requireBaseWorkspace(Workspace $workspace): Workspace { if (is_null($workspace->baseWorkspaceName)) { throw WorkspaceHasNoBaseWorkspaceName::butWasSupposedTo($workspace->workspaceName); } - $baseWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName($workspace->baseWorkspaceName); - if ($baseWorkspace === null) { + try { + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromWorkspaceName($workspace->baseWorkspaceName); + $baseWorkspace = $contentGraphAdapter->getWorkspace(); + } catch (WorkspaceDoesNotExist $_) { throw BaseWorkspaceDoesNotExist::butWasSupposedTo($workspace->workspaceName); } @@ -930,18 +940,19 @@ private function requireBaseWorkspace(Workspace $workspace, ContentRepository $c * @throws BaseWorkspaceEqualsWorkspaceException * @throws CircularRelationBetweenWorkspacesException */ - private function requireNonCircularRelationBetweenWorkspaces(Workspace $workspace, Workspace $baseWorkspace, ContentRepository $contentRepository): void + private function requireNonCircularRelationBetweenWorkspaces(Workspace $workspace, Workspace $baseWorkspace): void { if ($workspace->workspaceName->equals($baseWorkspace->workspaceName)) { throw new BaseWorkspaceEqualsWorkspaceException(sprintf('The base workspace of the target must be different from the given workspace "%s".', $workspace->workspaceName->value)); } $nextBaseWorkspace = $baseWorkspace; - while ($nextBaseWorkspace?->baseWorkspaceName !== null) { + while ($nextBaseWorkspace->baseWorkspaceName !== null) { if ($workspace->workspaceName->equals($nextBaseWorkspace->baseWorkspaceName)) { throw new CircularRelationBetweenWorkspacesException(sprintf('The workspace "%s" is already on the path of the target workspace "%s".', $workspace->workspaceName->value, $baseWorkspace->workspaceName->value)); } - $nextBaseWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName($nextBaseWorkspace->baseWorkspaceName); + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromWorkspaceName($nextBaseWorkspace->baseWorkspaceName); + $nextBaseWorkspace = $contentGraphAdapter->getWorkspace(); } } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php index 4d8d7e4a4a7..6ef12e1e5b6 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php @@ -87,25 +87,9 @@ public function findNodeAggregateById( ): ?NodeAggregate; /** - * Returns all node types in use, from the graph projection - * - * @return iterable - * @api - */ - public function findUsedNodeTypeNames(): iterable; - - /** - * @internal only for consumption inside the Command Handler - */ - public function findParentNodeAggregateByChildOriginDimensionSpacePoint( - ContentStreamId $contentStreamId, - NodeAggregateId $childNodeAggregateId, - OriginDimensionSpacePoint $childOriginDimensionSpacePoint - ): ?NodeAggregate; - - /** + * @param ContentStreamId $contentStreamId + * @param NodeAggregateId $childNodeAggregateId * @return iterable - * @internal only for consumption inside the Command Handler */ public function findParentNodeAggregates( ContentStreamId $contentStreamId, @@ -113,20 +97,10 @@ public function findParentNodeAggregates( ): iterable; /** + * @param ContentStreamId $contentStreamId + * @param NodeAggregateId $parentNodeAggregateId + * @param NodeName $name * @return iterable - * @internal only for consumption inside the Command Handler - */ - public function findChildNodeAggregates( - ContentStreamId $contentStreamId, - NodeAggregateId $parentNodeAggregateId - ): iterable; - - /** - * A node aggregate may have multiple child node aggregates with the same name - * as long as they do not share dimension space coverage - * - * @return iterable - * @internal only for consumption inside the Command Handler */ public function findChildNodeAggregatesByName( ContentStreamId $contentStreamId, @@ -135,24 +109,12 @@ public function findChildNodeAggregatesByName( ): iterable; /** - * @return iterable - * @internal only for consumption inside the Command Handler - */ - public function findTetheredChildNodeAggregates( - ContentStreamId $contentStreamId, - NodeAggregateId $parentNodeAggregateId - ): iterable; - - /** - * @internal only for consumption inside the Command Handler + * Returns all node types in use, from the graph projection + * + * @return iterable + * @api */ - public function getDimensionSpacePointsOccupiedByChildNodeName( - ContentStreamId $contentStreamId, - NodeName $nodeName, - NodeAggregateId $parentNodeAggregateId, - OriginDimensionSpacePoint $parentNodeOriginDimensionSpacePoint, - DimensionSpacePointSet $dimensionSpacePointsToCheck - ): DimensionSpacePointSet; + public function findUsedNodeTypeNames(): iterable; /** * @internal only for consumption in testcases diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentStream/ContentStreamFinder.php b/Neos.ContentRepository.Core/Classes/Projection/ContentStream/ContentStreamFinder.php index 6bcc6a9b2b0..1933d357817 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentStream/ContentStreamFinder.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentStream/ContentStreamFinder.php @@ -158,42 +158,4 @@ public function findUnusedAndRemovedContentStreams(): iterable )->fetchFirstColumn(); return array_map(ContentStreamId::fromString(...), $contentStreamIds); } - - public function findVersionForContentStream(ContentStreamId $contentStreamId): MaybeVersion - { - $connection = $this->client->getConnection(); - /* @var $state string|false */ - $version = $connection->executeQuery( - ' - SELECT version FROM ' . $this->tableName . ' - WHERE contentStreamId = :contentStreamId - ', - [ - 'contentStreamId' => $contentStreamId->value, - ] - )->fetchOne(); - - if ($version === false) { - return MaybeVersion::fromVersionOrNull(null); - } - - return MaybeVersion::fromVersionOrNull(Version::fromInteger($version)); - } - - public function hasContentStream(ContentStreamId $contentStreamId): bool - { - $connection = $this->client->getConnection(); - /* @var $state string|false */ - $version = $connection->executeQuery( - ' - SELECT version FROM ' . $this->tableName . ' - WHERE contentStreamId = :contentStreamId - ', - [ - 'contentStreamId' => $contentStreamId->value - ] - )->fetchOne(); - - return $version !== false; - } } diff --git a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php index fcdc1104af6..ee3c35464c6 100644 --- a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php +++ b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php @@ -4,12 +4,13 @@ namespace Neos\ContentRepository\StructureAdjustment\Adjustment; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\Common\NodeVariationInternals; use Neos\ContentRepository\Core\Feature\Common\TetheredNodeInternals; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterInterface; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterProvider; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMapping; use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMappings; @@ -21,12 +22,11 @@ use Neos\ContentRepository\Core\NodeType\NodeType; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; 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\EventStore\Model\EventStream\ExpectedVersion; class TetheredNodeAdjustments @@ -36,7 +36,7 @@ class TetheredNodeAdjustments use TetheredNodeInternals; public function __construct( - private readonly ContentRepository $contentRepository, + private readonly ContentGraphAdapterProvider $contentGraphAdapterProvider, private readonly ProjectedNodeIterator $projectedNodeIterator, private readonly NodeTypeManager $nodeTypeManager, private readonly DimensionSpace\InterDimensionalVariationGraph $interDimensionalVariationGraph, @@ -57,6 +57,8 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): \Generat $expectedTetheredNodes = $this->nodeTypeManager->getTetheredNodesConfigurationForNodeType($nodeType); foreach ($this->projectedNodeIterator->nodeAggregatesOfType($nodeTypeName) as $nodeAggregate) { + // TODO: We should use $nodeAggregate->workspaceName as soon as it's available + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromContentStreamId($nodeAggregate->contentStreamId); // find missing tethered nodes $foundMissingOrDisallowedTetheredNodes = false; $originDimensionSpacePoints = $nodeType->isOfType(NodeTypeName::ROOT_NODE_TYPE_NAME) @@ -69,14 +71,10 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): \Generat foreach ($expectedTetheredNodes as $tetheredNodeName => $expectedTetheredNodeType) { $tetheredNodeName = NodeName::fromString($tetheredNodeName); - $subgraph = $this->contentRepository->getContentGraph()->getSubgraph( - $nodeAggregate->contentStreamId, + $tetheredNode = $contentGraphAdapter->findChildNodeByNameInSubgraph( $originDimensionSpacePoint->toDimensionSpacePoint(), - VisibilityConstraints::withoutRestrictions() - ); - $tetheredNode = $subgraph->findNodeByPath( - $tetheredNodeName, $nodeAggregate->nodeAggregateId, + $tetheredNodeName, ); if ($tetheredNode === null) { $foundMissingOrDisallowedTetheredNodes = true; @@ -88,14 +86,14 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): \Generat $nodeAggregate->nodeAggregateId, StructureAdjustment::TETHERED_NODE_MISSING, 'The tethered child node "' . $tetheredNodeName->value . '" is missing.', - function () use ($nodeAggregate, $originDimensionSpacePoint, $tetheredNodeName, $expectedTetheredNodeType) { + function () use ($nodeAggregate, $originDimensionSpacePoint, $tetheredNodeName, $expectedTetheredNodeType, $contentGraphAdapter) { $events = $this->createEventsForMissingTetheredNode( + $contentGraphAdapter, $nodeAggregate, $originDimensionSpacePoint, $tetheredNodeName, null, - $expectedTetheredNodeType, - $this->contentRepository + $expectedTetheredNodeType ); $streamName = ContentStreamEventStreamName::fromContentStreamId($nodeAggregate->contentStreamId); @@ -114,8 +112,7 @@ function () use ($nodeAggregate, $originDimensionSpacePoint, $tetheredNodeName, } // find disallowed tethered nodes - $tetheredNodeAggregates = $this->contentRepository->getContentGraph()->findTetheredChildNodeAggregates( - $nodeAggregate->contentStreamId, + $tetheredNodeAggregates = $contentGraphAdapter->findTetheredChildNodeAggregates( $nodeAggregate->nodeAggregateId ); foreach ($tetheredNodeAggregates as $tetheredNodeAggregate) { @@ -137,12 +134,7 @@ function () use ($tetheredNodeAggregate) { // find wrongly ordered tethered nodes if ($foundMissingOrDisallowedTetheredNodes === false) { foreach ($originDimensionSpacePoints as $originDimensionSpacePoint) { - $subgraph = $this->contentRepository->getContentGraph()->getSubgraph( - $nodeAggregate->contentStreamId, - $originDimensionSpacePoint->toDimensionSpacePoint(), - VisibilityConstraints::withoutRestrictions() - ); - $childNodes = $subgraph->findChildNodes($nodeAggregate->nodeAggregateId, FindChildNodesFilter::create()); + $childNodes = $contentGraphAdapter->findChildNodesInSubgraph($originDimensionSpacePoint->toDimensionSpacePoint(), $nodeAggregate->nodeAggregateId); /** is indexed by node name, and the value is the tethered node itself */ $actualTetheredChildNodes = []; @@ -275,4 +267,9 @@ private function reorderNodes( ExpectedVersion::ANY() ); } + + protected function getContentGraphAdapter(WorkspaceName $workspaceName): ContentGraphAdapterInterface + { + return $this->contentGraphAdapterProvider->fromWorkspaceName($workspaceName); + } } diff --git a/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentService.php b/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentService.php index 3f50d52afe7..4208a3a4092 100644 --- a/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentService.php +++ b/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentService.php @@ -9,6 +9,7 @@ use Neos\ContentRepository\Core\EventStore\EventPersister; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterProvider; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; @@ -33,7 +34,8 @@ public function __construct( private readonly EventPersister $eventPersister, NodeTypeManager $nodeTypeManager, InterDimensionalVariationGraph $interDimensionalVariationGraph, - PropertyConverter $propertyConverter + PropertyConverter $propertyConverter, + ContentGraphAdapterProvider $contentGraphAdapterProvider ) { $projectedNodeIterator = new ProjectedNodeIterator( $contentRepository->getWorkspaceFinder(), @@ -41,7 +43,7 @@ public function __construct( ); $this->tetheredNodeAdjustments = new TetheredNodeAdjustments( - $contentRepository, + $contentGraphAdapterProvider, $projectedNodeIterator, $nodeTypeManager, $interDimensionalVariationGraph, diff --git a/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentServiceFactory.php b/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentServiceFactory.php index 3363a2ec7fb..6b09142cca7 100644 --- a/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentServiceFactory.php +++ b/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentServiceFactory.php @@ -19,7 +19,8 @@ public function build(ContentRepositoryServiceFactoryDependencies $serviceFactor $serviceFactoryDependencies->eventPersister, $serviceFactoryDependencies->nodeTypeManager, $serviceFactoryDependencies->interDimensionalVariationGraph, - $serviceFactoryDependencies->propertyConverter + $serviceFactoryDependencies->propertyConverter, + $serviceFactoryDependencies->contentGraphAdapterProvider ); } } diff --git a/Neos.ContentRepositoryRegistry.DoctrineDbalClient/Configuration/Objects.yaml b/Neos.ContentRepositoryRegistry.DoctrineDbalClient/Configuration/Objects.yaml index 1dfd0f4d332..e300debe9cc 100644 --- a/Neos.ContentRepositoryRegistry.DoctrineDbalClient/Configuration/Objects.yaml +++ b/Neos.ContentRepositoryRegistry.DoctrineDbalClient/Configuration/Objects.yaml @@ -9,3 +9,12 @@ Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjectionFactory: value: 'Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjectionFactory' 2: object: 'Neos\ContentRepository\Core\Infrastructure\DbalClientInterface' + +Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphAdapterFactoryBuilder: + scope: singleton + factoryObjectName: 'Neos\ContentRepositoryRegistry\Infrastructure\GenericObjectFactory' + arguments: + 1: + value: 'Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphAdapterFactoryBuilder' + 2: + object: 'Neos\ContentRepository\Core\Infrastructure\DbalClientInterface' diff --git a/Neos.ContentRepositoryRegistry.DoctrineDbalClient/Configuration/Settings.yaml b/Neos.ContentRepositoryRegistry.DoctrineDbalClient/Configuration/Settings.yaml index 2c39ca34a96..b13fcf70e46 100644 --- a/Neos.ContentRepositoryRegistry.DoctrineDbalClient/Configuration/Settings.yaml +++ b/Neos.ContentRepositoryRegistry.DoctrineDbalClient/Configuration/Settings.yaml @@ -7,3 +7,5 @@ Neos: # catchUpHooks for content cache flushing 'Neos.ContentRepository:ContentGraph': factoryObjectName: Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjectionFactory + contentGraphAdapterFactory: + factoryObjectName: Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphAdapterFactoryBuilder diff --git a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php index 25daca7cf84..6c5c63cd8d3 100644 --- a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php +++ b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php @@ -9,6 +9,9 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Factory\ProjectionsAndCatchUpHooksFactory; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterFactoryBuilderInterface; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterProviderFactoryInterface; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterProvider; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Projection\CatchUpHookFactoryInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; @@ -158,6 +161,7 @@ private function buildFactory(ContentRepositoryId $contentRepositoryId): Content $this->buildProjectionCatchUpTrigger($contentRepositoryId, $contentRepositorySettings), $this->buildUserIdProvider($contentRepositoryId, $contentRepositorySettings), $clock, + $this->buildContentGraphAdapterProvider($contentRepositoryId, $contentRepositorySettings), ); } catch (\Exception $exception) { throw InvalidConfigurationException::fromException($contentRepositoryId, $exception); @@ -281,4 +285,16 @@ private function buildClock(ContentRepositoryId $contentRepositoryIdentifier, ar } return $clockFactory->build($contentRepositoryIdentifier, $contentRepositorySettings['clock']['options'] ?? []); } + + /** @param array $contentRepositorySettings */ + private function buildContentGraphAdapterProvider(ContentRepositoryId $contentRepositoryIdentifier, array $contentRepositorySettings): ContentGraphAdapterFactoryBuilderInterface + { + isset($contentRepositorySettings['contentGraphAdapterFactory']['factoryObjectName']) || throw InvalidConfigurationException::fromMessage('Content repository "%s" does not have contentGraphAdapterFactory.factoryObjectName configured.', $contentRepositoryIdentifier->value); + $contentGraphAdapterFactoryBuilder = $this->objectManager->get($contentRepositorySettings['contentGraphAdapterFactory']['factoryObjectName']); + if (!$contentGraphAdapterFactoryBuilder instanceof ContentGraphAdapterFactoryBuilderInterface) { + throw InvalidConfigurationException::fromMessage('contentGraphAdapterFactory.factoryObjectName for content repository "%s" is not an instance of %s but %s.', $contentRepositoryIdentifier->value, ContentGraphAdapterFactoryBuilderInterface::class, get_debug_type($contentGraphAdapterFactoryBuilder)); + } + // TODO: Do we want to add options here? Would then have to be a setter I guess.... + return $contentGraphAdapterFactoryBuilder; + } } diff --git a/Neos.Neos/Classes/AssetUsage/Dto/AssetIdAndOriginalAssetId.php b/Neos.Neos/Classes/AssetUsage/Dto/AssetIdAndOriginalAssetId.php index d373d57f9cc..cfb347b0a6d 100644 --- a/Neos.Neos/Classes/AssetUsage/Dto/AssetIdAndOriginalAssetId.php +++ b/Neos.Neos/Classes/AssetUsage/Dto/AssetIdAndOriginalAssetId.php @@ -9,6 +9,7 @@ /** * @internal */ + #[Flow\Proxy(false)] readonly class AssetIdAndOriginalAssetId { diff --git a/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php b/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php index 7dbe858a5c5..acb7b5f1b08 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php +++ b/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php @@ -19,6 +19,7 @@ use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; +use Neos\ContentRepository\Core\Feature\ContentGraphAdapterProvider; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; use Neos\ContentRepository\Core\Feature\NodeRemoval\Command\RemoveNodeAggregate; @@ -39,7 +40,8 @@ public function __construct( private ContentRepository $contentRepository, private InterDimensionalVariationGraph $interDimensionalVariationGraph, - private NodeTypeManager $nodeTypeManager + private NodeTypeManager $nodeTypeManager, + private ContentGraphAdapterProvider $contentGraphAdapterProvider ) { } @@ -54,15 +56,18 @@ public function removeSiteNode(SiteNodeName $siteNodeName): void 1651921482 ); } - $contentGraph = $this->contentRepository->getContentGraph(); foreach ($this->contentRepository->getWorkspaceFinder()->findAll() as $workspace) { - $sitesNodeAggregate = $contentGraph->findRootNodeAggregateByType( - $workspace->currentContentStreamId, + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromWorkspaceName($workspace->workspaceName); + $sitesNodeAggregate = $contentGraphAdapter->findRootNodeAggregateByType( NodeTypeNameFactory::forSites() ); - $siteNodeAggregates = $contentGraph->findChildNodeAggregatesByName( - $workspace->currentContentStreamId, + + if (!$sitesNodeAggregate) { + continue; + } + + $siteNodeAggregates = $contentGraphAdapter->findChildNodeAggregatesByName( $sitesNodeAggregate->nodeAggregateId, $siteNodeName->toNodeName() ); @@ -99,8 +104,8 @@ public function createSiteNodeIfNotExists(Site $site, string $nodeTypeName): voi throw SiteNodeTypeIsInvalid::becauseItIsNotOfTypeSite(NodeTypeName::fromString($nodeTypeName)); } - $siteNodeAggregate = $this->contentRepository->getContentGraph()->findChildNodeAggregatesByName( - $liveWorkspace->currentContentStreamId, + $contentGraphAdapter = $this->contentGraphAdapterProvider->fromWorkspaceName($liveWorkspace->workspaceName); + $siteNodeAggregate = $contentGraphAdapter->findChildNodeAggregatesByName( $sitesNodeIdentifier, $site->getNodeName()->toNodeName(), ); diff --git a/Neos.Neos/Classes/Domain/Service/SiteServiceInternalsFactory.php b/Neos.Neos/Classes/Domain/Service/SiteServiceInternalsFactory.php index d47a105c5f1..e16177d77a6 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteServiceInternalsFactory.php +++ b/Neos.Neos/Classes/Domain/Service/SiteServiceInternalsFactory.php @@ -27,7 +27,8 @@ public function build(ContentRepositoryServiceFactoryDependencies $serviceFactor return new SiteServiceInternals( $serviceFactoryDependencies->contentRepository, $serviceFactoryDependencies->interDimensionalVariationGraph, - $serviceFactoryDependencies->nodeTypeManager + $serviceFactoryDependencies->nodeTypeManager, + $serviceFactoryDependencies->contentGraphAdapterProvider ); } }