kitsunet/Flowpack.SimpleSearch.ContentRepositoryAdaptor

View on GitHub
Classes/Indexer/NodeIndexer.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php
declare(strict_types=1);

namespace Flowpack\SimpleSearch\ContentRepositoryAdaptor\Indexer;

use Flowpack\SimpleSearch\Domain\Service\IndexInterface;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\ContentRepository\Domain\Repository\WorkspaceRepository;
use Neos\ContentRepository\Domain\Service\ContentDimensionPresetSourceInterface;
use Neos\ContentRepository\Domain\Service\ContextFactoryInterface;
use Neos\ContentRepository\Domain\Service\NodeTypeManager;
use Neos\ContentRepository\Exception\NodeException;
use Neos\ContentRepository\Search\Exception\IndexingException;
use Neos\ContentRepository\Search\Indexer\AbstractNodeIndexer;
use Neos\Eel\Exception;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Persistence\PersistenceManagerInterface;
use Neos\Flow\Security\Context;
use Symfony\Component\Yaml\Yaml;

/**
 * Indexer for Content Repository Nodes.
 *
 * @Flow\Scope("singleton")
 */
class NodeIndexer extends AbstractNodeIndexer
{
    /**
     * @Flow\Inject
     * @var IndexInterface
     */
    protected $indexClient;

    /**
     * @Flow\Inject
     * @var \Neos\ContentRepository\Search\Search\QueryBuilderInterface
     */
    protected $queryBuilder;

    /**
     * @Flow\Inject
     * @var PersistenceManagerInterface
     */
    protected $persistenceManager;

    /**
     * @Flow\Inject
     * @var NodeTypeManager
     */
    protected $nodeTypeManager;

    /**
     * @Flow\Inject
     * @var ContentDimensionPresetSourceInterface
     */
    protected $contentDimensionPresetSource;

    /**
     * @Flow\Inject
     * @var WorkspaceRepository
     */
    protected $workspaceRepository;

    /**
     * @Flow\Inject
     * @var ContextFactoryInterface
     */
    protected $contextFactory;

    /**
     * @Flow\Inject
     * @var Context
     */
    protected $securityContext;

    /**
     * @var array
     */
    protected $fulltextRootNodeTypes = [];

    /**
     * @var array
     */
    protected $indexedNodeData = [];

    /**
     * Called by the Flow object framework after creating the object and resolving all dependencies.
     *
     * @param integer $cause Creation cause
     * @throws \Neos\Flow\Configuration\Exception\InvalidConfigurationTypeException
     */
    public function initializeObject($cause)
    {
        parent::initializeObject($cause);
        foreach ($this->nodeTypeManager->getNodeTypes() as $nodeType) {
            $searchSettingsForNodeType = $nodeType->getConfiguration('search');
            if (is_array($searchSettingsForNodeType) && isset($searchSettingsForNodeType['fulltext']['isRoot']) && $searchSettingsForNodeType['fulltext']['isRoot'] === true) {
                $this->fulltextRootNodeTypes[] = $nodeType->getName();
            }
        }
    }

    /**
     * @return IndexInterface
     */
    public function getIndexClient(): IndexInterface
    {
        return $this->indexClient;
    }

    /**
     * index this node, and add it to the current bulk request.
     *
     * @param NodeInterface $node
     * @param string $targetWorkspaceName
     * @param boolean $indexVariants
     * @return void
     * @throws NodeException
     * @throws IndexingException
     * @throws Exception
     */
    public function indexNode(NodeInterface $node, $targetWorkspaceName = null, $indexVariants = true): void
    {
        if ($indexVariants === true) {
            $this->indexAllNodeVariants($node);
            return;
        }

        $identifier = $this->generateUniqueNodeIdentifier($node);

        if ($node->isRemoved()) {
            $this->indexClient->removeData($identifier);
            return;
        }

        $fulltextData = [];

        if (isset($this->indexedNodeData[$identifier]) && ($properties = $this->indexClient->findOneByIdentifier($identifier)) !== false) {
            unset($properties['__identifier__']);
            $properties['__workspace'] .= ', #' . ($targetWorkspaceName ?? $node->getContext()->getWorkspaceName()) . '#';
            if (array_key_exists('__dimensionshash', $properties)) {
                $properties['__dimensionshash'] .= ', #' . md5(json_encode($node->getContext()->getDimensions(), 0, 512)) . '#';
            } else {
                $properties['__dimensionshash'] = '#' . md5(json_encode($node->getContext()->getDimensions(), 0, 512)) . '#';
            }

            $this->indexClient->insertOrUpdatePropertiesToIndex($properties, $identifier);
        } else {
            $nodePropertiesToBeStoredInIndex = $this->extractPropertiesAndFulltext($node, $fulltextData);
            if (count($fulltextData) !== 0) {
                $this->addFulltextToRoot($node, $fulltextData);
            }

            $nodePropertiesToBeStoredInIndex = $this->postProcess($nodePropertiesToBeStoredInIndex);
            $this->indexClient->indexData($identifier, $nodePropertiesToBeStoredInIndex, $fulltextData);
            $this->indexedNodeData[$identifier] = $identifier;
        }
    }

    /**
     * @param NodeInterface $node
     * @return void
     */
    public function removeNode(NodeInterface $node): void
    {
        $identifier = $this->generateUniqueNodeIdentifier($node);
        $this->indexClient->removeData($identifier);
    }

    /**
     * @return void
     */
    public function flush(): void
    {
        $this->indexedNodeData = [];
    }

    /**
     * @param NodeInterface $node
     * @return void
     * @throws \Exception
     */
    protected function indexAllNodeVariants(NodeInterface $node): void
    {
        $nodeIdentifier = (string) $node->getNodeAggregateIdentifier();

        $allIndexedVariants = $this->indexClient->executeStatement(
            $this->queryBuilder->getFindIdentifiersByNodeIdentifierQuery('identifier'),
            [':identifier' => $nodeIdentifier]
        );
        foreach ($allIndexedVariants as $nodeVariant) {
            $this->indexClient->removeData($nodeVariant['__identifier__']);
        }

        foreach ($this->workspaceRepository->findAll() as $workspace) {
            $this->indexNodeInWorkspace($nodeIdentifier, $workspace->getName());
        }
    }

    /**
     * @param string $nodeIdentifier
     * @param string $workspaceName
     * @throws \Exception
     */
    protected function indexNodeInWorkspace(string $nodeIdentifier, string $workspaceName): void
    {
        $indexer = $this;
        $this->securityContext->withoutAuthorizationChecks(static function () use ($indexer, $nodeIdentifier, $workspaceName) {
            $dimensionCombinations = $indexer->calculateDimensionCombinations();
            if ($dimensionCombinations !== []) {
                foreach ($dimensionCombinations as $combination) {
                    $context = $indexer->contextFactory->create(['workspaceName' => $workspaceName, 'dimensions' => $combination]);
                    $node = $context->getNodeByIdentifier($nodeIdentifier);
                    if ($node !== null) {
                        $indexer->indexNode($node, null, false);
                    }
                }
            } else {
                $context = $indexer->contextFactory->create(['workspaceName' => $workspaceName]);
                $node = $context->getNodeByIdentifier($nodeIdentifier);
                if ($node !== null) {
                    $indexer->indexNode($node, null, false);
                }
            }
        });
    }

    /**
     * @param NodeInterface $node
     * @param array $fulltext
     */
    protected function addFulltextToRoot(NodeInterface $node, array $fulltext): void
    {
        $fulltextRoot = $this->findFulltextRoot($node);
        if ($fulltextRoot !== null) {
            $identifier = $this->generateUniqueNodeIdentifier($fulltextRoot);
            $this->indexClient->addToFulltext($fulltext, $identifier);
        }
    }

    /**
     * @param NodeInterface $node
     * @return NodeInterface
     */
    protected function findFulltextRoot(NodeInterface $node): ?NodeInterface
    {
        if (in_array($node->getNodeType()->getName(), $this->fulltextRootNodeTypes, true)) {
            return null;
        }

        try {
            $currentNode = $node->findParentNode();
            while ($currentNode !== null) {
                if (in_array($currentNode->getNodeType()->getName(), $this->fulltextRootNodeTypes, true)) {
                    return $currentNode;
                }

                $currentNode = $currentNode->findParentNode();
            }
        } catch (NodeException $exception) {
            return null;
        }

        return null;
    }

    /**
     * Generate identifier for index entry based on node identifier and context
     *
     * @param NodeInterface $node
     * @return string
     */
    protected function generateUniqueNodeIdentifier(NodeInterface $node): string
    {
        return $this->persistenceManager->getIdentifierByObject($node->getNodeData());
    }

    /**
     * @param array $nodePropertiesToBeStoredInIndex
     * @return array
     */
    protected function postProcess(array $nodePropertiesToBeStoredInIndex): array
    {
        foreach ($nodePropertiesToBeStoredInIndex as $propertyName => $propertyValue) {
            if (is_array($propertyValue)) {
                $nodePropertiesToBeStoredInIndex[$propertyName] = Yaml::dump($propertyValue);
            }
        }

        return $nodePropertiesToBeStoredInIndex;
    }

    /**
     * @return array
     */
    public function calculateDimensionCombinations(): array
    {
        $dimensionPresets = $this->contentDimensionPresetSource->getAllPresets();

        $dimensionValueCountByDimension = [];
        $possibleCombinationCount = 1;
        $combinations = [];

        foreach ($dimensionPresets as $dimensionName => $dimensionPreset) {
            if (isset($dimensionPreset['presets']) && !empty($dimensionPreset['presets'])) {
                $dimensionValueCountByDimension[$dimensionName] = count($dimensionPreset['presets']);
                $possibleCombinationCount *= $dimensionValueCountByDimension[$dimensionName];
            }
        }

        foreach ($dimensionPresets as $dimensionName => $dimensionPreset) {
            for ($i = 0; $i < $possibleCombinationCount; $i++) {
                if (!isset($combinations[$i]) || !is_array($combinations[$i])) {
                    $combinations[$i] = [];
                }

                $currentDimensionCurrentPreset = current($dimensionPresets[$dimensionName]['presets']);
                $combinations[$i][$dimensionName] = $currentDimensionCurrentPreset['values'];

                if (!next($dimensionPresets[$dimensionName]['presets'])) {
                    reset($dimensionPresets[$dimensionName]['presets']);
                }
            }
        }

        return $combinations;
    }
}