Skip to content

FEATURE: In-Memory Content Graph #5548

New issue

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

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

Already on GitHub? Sign in to your account

Draft
wants to merge 32 commits into
base: 9.1
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
360b894
[FIX] Find nodes marked as removed in other dimension
reissigMR Mar 6, 2025
6fae12d
TASK: Update translation files
weblate Mar 11, 2025
dac57eb
BUGFIX: Allow boolean values for hidden properties in nodetype schema
Sebobo Mar 18, 2025
cf78c38
bugfix: missing flash messages after Dropzone upload in Neos Media Br…
dos-format-d Mar 19, 2025
066abf1
Merge pull request #5523 from dos-format-d/bugfix/flash-messages-not-…
dlubitz Mar 20, 2025
944ed5f
Merge pull request #5521 from neos/bugfix/hidden-property
mhsdesign Mar 21, 2025
5db82d5
BUGFIX: Boot media module flash-messages correctly if rendered outsid…
mhsdesign Mar 24, 2025
44cb50c
Merge pull request #5528 from mhsdesign/bugfix/ensure-flashmessages-w…
nezaniel Mar 26, 2025
b8766e0
Merge pull request #5498 from beromir/fix/find-removed-nodes-in-other…
kitsunet Mar 27, 2025
1c9a35e
TASK: Add changelog for 8.3.23 [skip ci]
Mar 27, 2025
df5b683
BUGFIX: Allow back navigation in workspace ui
Sebobo Apr 3, 2025
6bc599e
TASK: Remove unused import
Sebobo Apr 3, 2025
9bd3afc
BUGFIX:Skip reference properties in AssetExportProcessor
kdambekalns Apr 4, 2025
9ec8716
Merge pull request #5543 from kdambekalns/bugfix/skip-references-in-a…
kdambekalns Apr 4, 2025
2a1c83d
TASK: Add changelog for 9.0.2 [skip ci]
Apr 8, 2025
079992d
TASK: Allow neos/fusion-form in version 3.x to support neos/symfonyma…
dlubitz Apr 16, 2025
e147d9d
Bugfix/5373: Fix publishing for `Neos.Neos:RestrictedEditor` from 3rd…
so-grimm Apr 17, 2025
de75150
WIP: Make InMemoryGraph pass RootNodeCreation
Apr 20, 2025
94bf59e
Make InMemoryContentGraph pass the NodeCreation test suite
Apr 21, 2025
293c585
Refactor to general hierarchy hyperrelations
Apr 22, 2025
465defb
Bugfix/5373: adjust indentation for comments to fix styleCI issue
so-grimm Apr 22, 2025
8d88395
Merge pull request #5546 from queoGmbH/bugfix/5373_restrictedEditor-p…
dlubitz Apr 28, 2025
a7d65c3
WIP: Implement node variation
May 2, 2025
657f681
TASK: Remove support for neos/fusion-form 1.x
dlubitz May 5, 2025
bc688be
Merge pull request #5544 from dlubitz/task/fusion-form-version
dlubitz May 5, 2025
5db613b
BUGFIX: Replace skip migrations with simple return
dlubitz May 7, 2025
6cf24cd
Merge pull request #5555 from dlubitz/bugfix/replace-skipif-migrations
kitsunet May 9, 2025
0b4e167
Merge branch '8.3' into 8.4
dlubitz May 14, 2025
6bd96db
Merge branch '8.4' into 9.0
dlubitz May 14, 2025
7377e18
TASK: Add changelog for 9.0.3 [skip ci]
May 14, 2025
c52e85d
Merge pull request #5539 from neos/bugfix/workspace-ui-history
markusguenther May 20, 2025
fc6efc9
Merge branch '9.0' into inMemoryContentGraph
May 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\Core\Projection\ContentGraph\InMemoryContentGraph;

use Neos\ContentRepository\Core\Factory\SubscriberFactoryDependencies;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphProjectionFactoryInterface;
use Neos\ContentRepository\Core\Projection\ContentGraph\InMemoryContentGraph\Projection\InMemoryContentGraphStructure;
use Neos\ContentRepository\Core\Projection\ContentGraph\InMemoryContentGraph\Repository\InMemoryContentStreamRegistry;
use Neos\ContentRepository\Core\Projection\ContentGraph\InMemoryContentGraph\Repository\InMemoryWorkspaceRegistry;
use Neos\ContentRepository\Core\Projection\ContentGraph\InMemoryContentGraph\Repository\NodeFactory;

/**
* Use this class as ProjectionFactory in your configuration to construct a content graph
*
* @api
*/
final class InMemoryContentGraphProjectionFactory implements ContentGraphProjectionFactoryInterface
{
public function build(
SubscriberFactoryDependencies $projectionFactoryDependencies,
): InMemoryContentGraphProjection {
$nodeFactory = new NodeFactory(
$projectionFactoryDependencies->contentRepositoryId,
$projectionFactoryDependencies->getPropertyConverter(),
);

$graphStructure = InMemoryContentGraphStructure::getInstance();
$workspaceRegistry = InMemoryWorkspaceRegistry::getInstance();
$contentStreamRegistry = InMemoryContentStreamRegistry::getInstance();

$contentGraphReadModel = new InMemoryContentGraphReadModelAdapter(
$projectionFactoryDependencies->contentRepositoryId,
$projectionFactoryDependencies->nodeTypeManager,
$graphStructure,
$workspaceRegistry,
$contentStreamRegistry,
$nodeFactory,
);

return new InMemoryContentGraphProjection(
$contentGraphReadModel,
$graphStructure,
$workspaceRegistry,
$contentStreamRegistry,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\Core\Projection\ContentGraph\InMemoryContentGraph;

use Neos\ContentRepository\Core\NodeType\NodeTypeManager;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface;
use Neos\ContentRepository\Core\Projection\ContentGraph\InMemoryContentGraph\Projection\InMemoryContentGraphStructure;
use Neos\ContentRepository\Core\Projection\ContentGraph\InMemoryContentGraph\Projection\InMemoryContentStreamRecord;
use Neos\ContentRepository\Core\Projection\ContentGraph\InMemoryContentGraph\Projection\InMemoryWorkspaceRecord;
use Neos\ContentRepository\Core\Projection\ContentGraph\InMemoryContentGraph\Repository\InMemoryContentGraph;
use Neos\ContentRepository\Core\Projection\ContentGraph\InMemoryContentGraph\Repository\InMemoryContentStreamRegistry;
use Neos\ContentRepository\Core\Projection\ContentGraph\InMemoryContentGraph\Repository\InMemoryWorkspaceRegistry;
use Neos\ContentRepository\Core\Projection\ContentGraph\InMemoryContentGraph\Repository\NodeFactory;
use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId;
use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStream;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
use Neos\ContentRepository\Core\SharedModel\Workspace\Workspace;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
use Neos\ContentRepository\Core\SharedModel\Workspace\Workspaces;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceStatus;

/**
* @internal
*/
final readonly class InMemoryContentGraphReadModelAdapter implements ContentGraphReadModelInterface
{
public function __construct(
private ContentRepositoryId $contentRepositoryId,
private NodeTypeManager $nodeTypeManager,
private InMemoryContentGraphStructure $graphStructure,
private InMemoryWorkspaceRegistry $workspaceRegistry,
private InMemoryContentStreamRegistry $contentStreamRegistry,
private NodeFactory $nodeFactory,
) {
}

/**
* @throws WorkspaceDoesNotExist if the workspace does not exist
*/
public function getContentGraph(WorkspaceName $workspaceName): InMemoryContentGraph
{
$workspace = $this->workspaceRegistry->workspaces[$workspaceName->value] ?? null;
if ($workspace === null) {
throw WorkspaceDoesNotExist::butWasSupposedTo($workspaceName);
}
return new InMemoryContentGraph(
$this->graphStructure,
$this->nodeFactory,
$this->contentRepositoryId,
$this->nodeTypeManager,
$workspaceName,
$workspace->currentContentStreamId,
);
}

public function findWorkspaceByName(WorkspaceName $workspaceName): ?Workspace
{
$workspaceRecord = $this->workspaceRegistry->workspaces[$workspaceName->value] ?? null;

return $workspaceRecord
? self::mapWorkspaceRecordToWorkspace($workspaceRecord)
: null;
}

public function findWorkspaces(): Workspaces
{
return Workspaces::fromArray(array_map(
fn (InMemoryWorkspaceRecord $workspaceRecord): Workspace
=> self::mapWorkspaceRecordToWorkspace($workspaceRecord),
$this->workspaceRegistry->workspaces
));
}

public function findContentStreamById(ContentStreamId $contentStreamId): ?ContentStream
{
$contentStreamRecord = $this->contentStreamRegistry->contentStreams[$contentStreamId->value] ?? null;

return $contentStreamRecord
? self::mapContentStreamRecordToContentStream($contentStreamRecord)
: null;
}

public function countNodes(): int
{
return $this->graphStructure->totalNodeCount;
}

private static function mapWorkspaceRecordToWorkspace(InMemoryWorkspaceRecord $workspaceRecord): Workspace
{
return Workspace::create(
$workspaceRecord->workspaceName,
$workspaceRecord->baseWorkspaceName,
$workspaceRecord->currentContentStreamId,
$workspaceRecord->baseWorkspaceName === null || $workspaceRecord->isUpToDateWithBase
? WorkspaceStatus::UP_TO_DATE
: WorkspaceStatus::OUTDATED,
$workspaceRecord->baseWorkspaceName !== null && $workspaceRecord->hasChanges
);
}

private static function mapContentStreamRecordToContentStream(InMemoryContentStreamRecord $contentStreamRecord): ContentStream
{
return ContentStream::create(
$contentStreamRecord->id,
$contentStreamRecord->sourceContentStreamId,
$contentStreamRecord->version,
$contentStreamRecord->isClosed,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<?php

declare(strict_types=1);

namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature;

use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\HierarchyRelation;
use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRecord;
use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint;
use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet;
use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSibling;
use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSiblings;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;

/**
* The NodeMove projection feature trait
*
* @internal
*/
trait NodeMove
{
use SubtreeTagging;

private function moveNodeAggregate(ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, ?NodeAggregateId $newParentNodeAggregateId, InterdimensionalSiblings $succeedingSiblingsForCoverage): void
{
foreach ($succeedingSiblingsForCoverage as $succeedingSiblingForCoverage) {
$nodeToBeMoved = $this->projectionContentGraph->findNodeInAggregate(
$contentStreamId,
$nodeAggregateId,
$succeedingSiblingForCoverage->dimensionSpacePoint
);

if (is_null($nodeToBeMoved)) {
throw new \RuntimeException(sprintf('Failed to move node "%s" in sub graph %s@%s because it does not exist', $nodeAggregateId->value, $succeedingSiblingForCoverage->dimensionSpacePoint->toJson(), $contentStreamId->value), 1716471638);
}

if ($newParentNodeAggregateId) {
$this->moveNodeBeneathParent(
$contentStreamId,
$nodeToBeMoved,
$newParentNodeAggregateId,
$succeedingSiblingForCoverage
);
$this->moveSubtreeTags(
$contentStreamId,
$newParentNodeAggregateId,
$succeedingSiblingForCoverage->dimensionSpacePoint
);
} else {
$this->moveNodeBeforeSucceedingSibling(
$contentStreamId,
$nodeToBeMoved,
$succeedingSiblingForCoverage,
);
// subtree tags stay the same if the parent doesn't change
}
}
}

/**
* This helper is responsible for moving a single incoming HierarchyRelation of $nodeToBeMoved
* to a new location without changing the parent. $succeedingSiblingForCoverage specifies
* which incoming HierarchyRelation should be moved and where exactly.
*
* The move target is given as $succeedingSiblingNodeMoveTarget. This also specifies the new parent node.
*/
private function moveNodeBeforeSucceedingSibling(
ContentStreamId $contentStreamId,
NodeRecord $nodeToBeMoved,
InterdimensionalSibling $succeedingSiblingForCoverage,
): void {
// find the single ingoing hierarchy relation which we want to move
$ingoingHierarchyRelation = $this->findIngoingHierarchyRelationToBeMoved(
$nodeToBeMoved,
$contentStreamId,
$succeedingSiblingForCoverage->dimensionSpacePoint
);

$newSucceedingSibling = null;
if ($succeedingSiblingForCoverage->nodeAggregateId) {
// find the new succeeding sibling NodeRecord; We need this record because we'll use its RelationAnchorPoint later.
$newSucceedingSibling = $this->projectionContentGraph->findNodeInAggregate(
$contentStreamId,
$succeedingSiblingForCoverage->nodeAggregateId,
$succeedingSiblingForCoverage->dimensionSpacePoint
);
if ($newSucceedingSibling === null) {
throw new \RuntimeException(sprintf('Failed to move node "%s" in sub graph %s@%s because target succeeding sibling node "%s" is missing', $nodeToBeMoved->nodeAggregateId->value, $succeedingSiblingForCoverage->dimensionSpacePoint->toJson(), $contentStreamId->value, $succeedingSiblingForCoverage->nodeAggregateId->value), 1716471881);
}
}

// fetch...
$newPosition = $this->getRelationPosition(
$ingoingHierarchyRelation->parentNodeAnchor,
null,
$newSucceedingSibling?->relationAnchorPoint,
$contentStreamId,
$succeedingSiblingForCoverage->dimensionSpacePoint
);

// ...and assign the new position
$ingoingHierarchyRelation->assignNewPosition(
$newPosition,
$this->dbal,
$this->tableNames
);
}

/**
* This helper is responsible for moving a single incoming HierarchyRelation of $nodeToBeMoved
* to a new location including a change of parent. $succeedingSiblingForCoverage specifies
* which incoming HierarchyRelation should be moved and where exactly.
*
* The move target is given as $parentNodeAggregateId and $succeedingSiblingForCoverage.
* We always move beneath the parent before the succeeding sibling if given (or to the end)
*/
private function moveNodeBeneathParent(
ContentStreamId $contentStreamId,
NodeRecord $nodeToBeMoved,
NodeAggregateId $parentNodeAggregateId,
InterdimensionalSibling $succeedingSiblingForCoverage,
): void {
// find the single ingoing hierarchy relation which we want to move
$ingoingHierarchyRelation = $this->findIngoingHierarchyRelationToBeMoved(
$nodeToBeMoved,
$contentStreamId,
$succeedingSiblingForCoverage->dimensionSpacePoint
);

// find the new parent NodeRecord; We need this record because we'll use its RelationAnchorPoints later.
$newParent = $this->projectionContentGraph->findNodeInAggregate(
$contentStreamId,
$parentNodeAggregateId,
$succeedingSiblingForCoverage->dimensionSpacePoint
);
if ($newParent === null) {
throw new \RuntimeException(sprintf('Failed to move node "%s" in sub graph %s@%s because target parent node is missing', $nodeToBeMoved->nodeAggregateId->value, $succeedingSiblingForCoverage->dimensionSpacePoint->toJson(), $contentStreamId->value), 1716471955);
}

$newSucceedingSibling = null;
if ($succeedingSiblingForCoverage->nodeAggregateId) {
// find the new succeeding sibling NodeRecord; We need this record because we'll use its RelationAnchorPoint later.
$newSucceedingSibling = $this->projectionContentGraph->findNodeInAggregate(
$contentStreamId,
$succeedingSiblingForCoverage->nodeAggregateId,
$succeedingSiblingForCoverage->dimensionSpacePoint
);
if ($newSucceedingSibling === null) {
throw new \RuntimeException(sprintf('Failed to move node "%s" in sub graph %s@%s because target succeeding sibling node is missing', $nodeToBeMoved->nodeAggregateId->value, $succeedingSiblingForCoverage->dimensionSpacePoint->toJson(), $contentStreamId->value), 1716471995);
}
}

// assign new position
$newPosition = $this->getRelationPosition(
$newParent->relationAnchorPoint,
null,
$newSucceedingSibling?->relationAnchorPoint,
$contentStreamId,
$succeedingSiblingForCoverage->dimensionSpacePoint
);

// this is the actual move
$ingoingHierarchyRelation->assignNewParentNode(
$newParent->relationAnchorPoint,
$newPosition,
$this->dbal,
$this->tableNames
);
}

/**
* Helper for the move methods.
*
* @param NodeRecord $nodeToBeMoved
* @param ContentStreamId $contentStreamId
* @param DimensionSpacePoint $coveredDimensionSpacePointWhereMoveShouldHappen
* @return HierarchyRelation
*/
private function findIngoingHierarchyRelationToBeMoved(
NodeRecord $nodeToBeMoved,
ContentStreamId $contentStreamId,
DimensionSpacePoint $coveredDimensionSpacePointWhereMoveShouldHappen
): HierarchyRelation {
$restrictToSet = DimensionSpacePointSet::fromArray([$coveredDimensionSpacePointWhereMoveShouldHappen]);
$ingoingHierarchyRelations = $this->projectionContentGraph->findIngoingHierarchyRelationsForNode(
$nodeToBeMoved->relationAnchorPoint,
$contentStreamId,
$restrictToSet,
);
if (count($ingoingHierarchyRelations) !== 1) {
// there should always be exactly one incoming relation in the given DimensionSpacePoint; everything
// else would be a totally wrong behavior of findIngoingHierarchyRelationsForNode().
throw new \RuntimeException(sprintf('Failed move node "%s" in sub graph %s@%s because ingoing source hierarchy relation is missing', $nodeToBeMoved->nodeAggregateId->value, $restrictToSet->toJson(), $contentStreamId->value), 1716472138);
}
return reset($ingoingHierarchyRelations);
}
}
Loading
Loading