Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

FEATURE: Speed up UI flow queries with custom queries #3817

Draft
wants to merge 12 commits into
base: 8.4
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 1 addition & 22 deletions Classes/Aspects/AugmentationAspect.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,12 @@ class AugmentationAspect
*/
protected $nodeAuthorizationService;

/**
* @Flow\Inject
* @var UserLocaleService
*/
protected $userLocaleService;

/**
* @Flow\Inject
* @var HtmlAugmenter
*/
protected $htmlAugmenter;

/**
* @Flow\Inject
* @var NodeInfoHelper
*/
protected $nodeInfoHelper;

/**
* @Flow\Inject
* @var SessionInterface
Expand Down Expand Up @@ -126,16 +114,7 @@ public function contentElementAugmentation(JoinPointInterface $joinPoint)
$attributes['data-__neos-node-contextpath'] = $node->getContextPath();
$attributes['data-__neos-fusion-path'] = $fusionPath;

$this->userLocaleService->switchToUILocale();

$serializedNode = json_encode($this->nodeInfoHelper->renderNodeWithPropertiesAndChildrenInformation($node, $this->controllerContext));

$this->userLocaleService->switchToUILocale(true);

$wrappedContent = $this->htmlAugmenter->addAttributes($content, $attributes, 'div');
$wrappedContent .= "<script data-neos-nodedata>(function(){(this['@Neos.Neos.Ui:Nodes'] = this['@Neos.Neos.Ui:Nodes'] || {})['{$node->getContextPath()}'] = {$serializedNode}})()</script>";

return $wrappedContent;
return $this->htmlAugmenter->addAttributes($content, $attributes);
}

/**
Expand Down
280 changes: 274 additions & 6 deletions Classes/ContentRepository/Service/NodeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@
* source code.
*/

use Doctrine\ORM\EntityManagerInterface;
use Neos\ContentRepository\Domain\Factory\NodeFactory;
use Neos\ContentRepository\Domain\Model\NodeData;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\ContentRepository\Domain\Model\Workspace;
use Neos\ContentRepository\Domain\Repository\NodeDataRepository;
use Neos\ContentRepository\Domain\Service\Context;
use Neos\ContentRepository\Domain\Service\ContextFactoryInterface;
use Neos\ContentRepository\Domain\Utility\NodePaths;
use Neos\Eel\FlowQuery\FlowQuery;
Expand Down Expand Up @@ -46,6 +51,35 @@ class NodeService
*/
protected $domainRepository;

/**
* @Flow\Inject
* @var NodeDataRepository
*/
protected $nodeDataRepository;

/**
* @Flow\Inject
* @var EntityManagerInterface
*/
protected $entityManager;

/**
* @Flow\Inject
* @var NodeFactory
*/
protected $nodeFactory;

/**
* @var array<string, Context>
*/
protected array $contextCache = [];

/**
* @Flow\InjectConfiguration(path="nodeTypeRoles.ignored", package="Neos.Neos.Ui")
* @var string
*/
protected $ignoredNodeTypeRole;

/**
* Helper method to retrieve the closest document for a node
*
Expand Down Expand Up @@ -86,11 +120,72 @@ public function getNodeFromContextPath($contextPath, Site $site = null, Domain $
$nodePath = $nodePathAndContext['nodePath'];
$workspaceName = $nodePathAndContext['workspaceName'];
$dimensions = $nodePathAndContext['dimensions'];
$siteNodeName = $site ? $site->getNodeName() : explode('/', $nodePath)[2];

// Prevent reloading the same context multiple times
$contextHash = md5(implode('|', [$siteNodeName, $workspaceName, json_encode($dimensions), $includeAll]));
if (isset($this->contextCache[$contextHash])) {
$context = $this->contextCache[$contextHash];
} else {
$contextProperties = $this->prepareContextProperties($workspaceName, $dimensions);

if ($site === null) {
$site = $this->siteRepository->findOneByNodeName($siteNodeName);
}

if ($domain === null) {
$domain = $this->domainRepository->findOneBySite($site);
}

$contextProperties['currentSite'] = $site;
$contextProperties['currentDomain'] = $domain;
if ($includeAll === true) {
$contextProperties['invisibleContentShown'] = true;
$contextProperties['removedContentShown'] = true;
$contextProperties['inaccessibleContentShown'] = true;
}

$context = $this->contextFactory->create(
$contextProperties
);

$workspace = $context->getWorkspace(false);
if (!$workspace) {
return new Error(
sprintf('Could not convert the given source to Node object because the workspace "%s" as specified in the context node path does not exist.', $workspaceName),
1451392329
);
}
$this->contextCache[$contextHash] = $context;
}

return $context->getNode($nodePath);
}

/**
* Converts given context paths to a node objects
*
* @param string[] $nodeContextPaths
* @return NodeInterface[]|Error
*/
public function getNodesFromContextPaths(array $nodeContextPaths, Site $site = null, Domain $domain = null, $includeAll = false): array|Error
{
if (!$nodeContextPaths) {
return [];
}

$nodePaths = array_map(static function($nodeContextPath) {
return NodePaths::explodeContextPath($nodeContextPath)['nodePath'];
}, $nodeContextPaths);

$nodePathAndContext = NodePaths::explodeContextPath($nodeContextPaths[0]);
$nodePath = $nodePathAndContext['nodePath'];
$workspaceName = $nodePathAndContext['workspaceName'];
$dimensions = $nodePathAndContext['dimensions'];
$siteNodeName = explode('/', $nodePath)[2];
$contextProperties = $this->prepareContextProperties($workspaceName, $dimensions);

if ($site === null) {
list(, , $siteNodeName) = explode('/', $nodePath);
$site = $this->siteRepository->findOneByNodeName($siteNodeName);
}

Expand All @@ -105,10 +200,7 @@ public function getNodeFromContextPath($contextPath, Site $site = null, Domain $
$contextProperties['removedContentShown'] = true;
$contextProperties['inaccessibleContentShown'] = true;
}

$context = $this->contextFactory->create(
$contextProperties
);
$context = $this->contextFactory->create($contextProperties);

$workspace = $context->getWorkspace(false);
if (!$workspace) {
Expand All @@ -118,7 +210,183 @@ public function getNodeFromContextPath($contextPath, Site $site = null, Domain $
);
}

return $context->getNode($nodePath);
// Query nodes and their variants from the database
$queryBuilder = $this->entityManager->createQueryBuilder();
$workspaces = $this->collectWorkspaceAndAllBaseWorkspaces($workspace);
$workspacesNames = array_map(static function(Workspace $workspace) { return $workspace->getName(); }, $workspaces);

// Filter by workspace and its parents
$queryBuilder->select('n')
->from(NodeData::class, 'n')
->where('n.workspace IN (:workspaces)')
->andWhere('n.movedTo IS NULL')
->andWhere('n.path IN (:nodePaths)')
->setParameter('workspaces', $workspacesNames)
->setParameter('nodePaths', $nodePaths);
$query = $queryBuilder->getQuery();
$nodeDataWithVariants = $query->getResult();

// Remove node duplicates
$reducedNodeData = $this->reduceNodeVariantsByWorkspacesAndDimensions($nodeDataWithVariants, $workspaces, $dimensions);

// Convert nodedata objects to nodes
return array_reduce($reducedNodeData, function (array $carry, NodeData $nodeData) use ($context) {
$node = $this->nodeFactory->createFromNodeData($nodeData, $context);
if ($node !== null) {
$carry[] = $node;
}
$context->getFirstLevelNodeCache()->setByPath($node->getPath(), $node);
return $carry;
}, []);
}

/**
* @param NodeInterface[] $parentNodes
*/
public function preloadChildNodesForNodes(array $parentNodes): void
{
if (empty($parentNodes)) {
return;
}

$workspace = $parentNodes[0]->getWorkspace();
$context = $parentNodes[0]->getContext();
$dimensions = $context->getDimensions();

$parentPaths = array_map(static function(NodeInterface $parentNode) {
return $parentNode->getPath();
}, $parentNodes);

// Query nodes and their variants from the database
$queryBuilder = $this->entityManager->createQueryBuilder();
$workspaces = $this->collectWorkspaceAndAllBaseWorkspaces($workspace);
$workspacesNames = array_map(static function(Workspace $workspace) { return $workspace->getName(); }, $workspaces);

// Filter by workspace and its parents
$queryBuilder->select('n')
->from(NodeData::class, 'n')
->where('n.workspace IN (:workspaces)')
->andWhere('n.movedTo IS NULL')
->andWhere('n.parentPath IN (:parentPaths)')
->setParameter('workspaces', $workspacesNames)
->setParameter('parentPaths', $parentPaths);
$query = $queryBuilder->getQuery();
$nodeDataWithVariants = $query->getResult();

// Remove node duplicates
$reducedNodeData = $this->reduceNodeVariantsByWorkspacesAndDimensions(
$nodeDataWithVariants,
$workspaces,
$dimensions
);

// Convert nodedata objects to nodes and group them by parent path
$childNodesByParentPath = array_reduce($reducedNodeData, function (array $carry, NodeData $nodeData) use ($context) {
$node = $this->nodeFactory->createFromNodeData($nodeData, $context);
if ($node !== null) {
if (!isset($carry[$node->getParentPath()])) {
$carry[$node->getParentPath()] = [$node];
} else {
$carry[$node->getParentPath()][] = $node;
}
}
return $carry;
}, []);

foreach ($childNodesByParentPath as $parentPath => $childNodes) {
usort($childNodes, static function(NodeInterface $a, NodeInterface $b) {
return $a->getIndex() <=> $b->getIndex();
});
$context->getFirstLevelNodeCache()->setChildNodesByPathAndNodeTypeFilter(
$parentPath, '!' .
$this->ignoredNodeTypeRole,
$childNodes
);
}
}

/**
* Given an array with duplicate nodes (from different workspaces and dimensions) those are reduced to uniqueness (by node identifier)
* Copied from Neos\ContentRepository\Domain\Repository\NodeDataRepository
*
* @param NodeData[] $nodes NodeData result with multiple and duplicate identifiers (different nodes and redundant results for node variants with different dimensions)
* @param Workspace[] $workspaces
* @param array $dimensions
* @return NodeData[] Array of unique node results indexed by identifier
*/
protected function reduceNodeVariantsByWorkspacesAndDimensions(array $nodes, array $workspaces, array $dimensions): array
{
$reducedNodes = [];

$minimalDimensionPositionsByIdentifier = [];

$workspaceNames = array_map(static fn (Workspace $workspace) => $workspace->getName(), $workspaces);

foreach ($nodes as $node) {
$nodeDimensions = $node->getDimensionValues();

// Find the position of the workspace, a smaller value means more priority
$workspacePosition = array_search($node->getWorkspace()->getName(), $workspaceNames);
if ($workspacePosition === false) {
throw new \Exception(sprintf(
'Node workspace "%s" not found in allowed workspaces (%s), this could result from a detached workspace entity in the context.',
$node->getWorkspace()->getName(),
implode(', ', $workspaceNames)
), 1718740117);
}

// Find positions in dimensions, add workspace in front for highest priority
$dimensionPositions = [];

// Special case for no dimensions
if ($dimensions === []) {
// We can just decide if the given node has no dimensions.
$dimensionPositions[] = ($nodeDimensions === []) ? 0 : 1;
}

foreach ($dimensions as $dimensionName => $dimensionValues) {
if (isset($nodeDimensions[$dimensionName])) {
foreach ($nodeDimensions[$dimensionName] as $nodeDimensionValue) {
$position = array_search($nodeDimensionValue, $dimensionValues);
if ($position === false) {
$position = PHP_INT_MAX;
}
$dimensionPositions[$dimensionName] = isset($dimensionPositions[$dimensionName]) ? min(
$dimensionPositions[$dimensionName],
$position
) : $position;
}
} else {
$dimensionPositions[$dimensionName] = isset($dimensionPositions[$dimensionName]) ? min(
$dimensionPositions[$dimensionName],
PHP_INT_MAX
) : PHP_INT_MAX;
}
}
$dimensionPositions[] = $workspacePosition;

$identifier = $node->getIdentifier();
// Yes, it seems to work comparing arrays that way!
if (!isset($minimalDimensionPositionsByIdentifier[$identifier]) || $dimensionPositions < $minimalDimensionPositionsByIdentifier[$identifier]) {
$reducedNodes[$identifier] = $node;
$minimalDimensionPositionsByIdentifier[$identifier] = $dimensionPositions;
}
}

return $reducedNodes;
}

/**
* @return Workspace[]
*/
protected function collectWorkspaceAndAllBaseWorkspaces(Workspace $workspace): array
{
$workspaces = [];
while ($workspace !== null) {
$workspaces[] = $workspace;
$workspace = $workspace->getBaseWorkspace();
}
return $workspaces;
}

/**
Expand Down
Loading
Loading