Smile-SA/elasticsuite

View on GitHub
src/module-elasticsuite-catalog-graph-ql/DataProvider/Product/LayeredNavigation/Builder/Attribute.php

Summary

Maintainability
D
1 day
Test Coverage
<?php

/**
 * DISCLAIMER
 * Do not edit or add to this file if you wish to upgrade Smile Elastic Suite to newer
 * versions in the future.
 *
 * @category  Smile
 * @package   Smile\ElasticsuiteCatalogGraphQl
 * @author    Romain Ruaud <romain.ruaud@smile.fr>
 * @copyright 2020 Smile
 * @license   Open Software License ("OSL") v. 3.0
 */

namespace Smile\ElasticsuiteCatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder;

use Magento\Catalog\Model\Product\Attribute\Repository as AttributeRepository;
use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Formatter\LayerFormatter;
use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilderInterface;
use Magento\Eav\Api\Data\AttributeInterface;
use Magento\Framework\Api\Search\AggregationInterface;
use Magento\Framework\Api\Search\BucketInterface;
use Smile\ElasticsuiteCore\Helper\Mapping;
use Smile\ElasticsuiteCore\Search\Request\BucketInterface as ElasticBucketInterface;
use Smile\ElasticsuiteCatalog\Model\Attribute\LayeredNavAttributesProvider;

/**
 * Layered Navigation Builder for Default Attribute.
 *
 * @category Smile
 * @package  Smile\ElasticsuiteCatalogGraphQl
 * @author   Romain Ruaud <romain.ruaud@smile.fr>
 */
class Attribute implements LayerBuilderInterface
{
    /**
     * @var LayerFormatter
     */
    private $layerFormatter;

    /**
     * @var \Magento\Catalog\Model\Product\Attribute\Repository
     */
    private $attributeRepository;

    /**
     * @var LayeredNavAttributesProvider
     */
    protected $layeredNavAttributesProvider;

    /**
     * @var array
     */
    protected $hideNoValueAttributes;

    /**
     * @var array
     */
    private $bucketNameFilter = [
        Price::PRICE_BUCKET,
        Category::CATEGORY_BUCKET,
        'attribute_set_id',
        'indexed_attributes',
    ];

    /**
     * @param LayerFormatter               $layerFormatter               Layer Formatter.
     * @param AttributeRepository          $attributeRepository          Attribute Repository.
     * @param LayeredNavAttributesProvider $layeredNavAttributesProvider Layered nav attributes provider.
     * @param array                        $hideNoValueAttributes        Attributes for which we must hide the value no.
     * @param array                        $bucketNameFilter             Bucket Filter.
     */
    public function __construct(
        LayerFormatter $layerFormatter,
        AttributeRepository $attributeRepository,
        LayeredNavAttributesProvider $layeredNavAttributesProvider,
        $hideNoValueAttributes = [],
        $bucketNameFilter = []
    ) {
        $this->layerFormatter               = $layerFormatter;
        $this->bucketNameFilter             = \array_merge($this->bucketNameFilter, $bucketNameFilter);
        $this->attributeRepository          = $attributeRepository;
        $this->layeredNavAttributesProvider = $layeredNavAttributesProvider;
        $this->hideNoValueAttributes        = $hideNoValueAttributes;
    }

    /**
     * {@inheritdoc}
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     * @SuppressWarnings(PHPMD.NPathComplexity)
     * @throws \Zend_Db_Statement_Exception
     */
    public function build(AggregationInterface $aggregation, ?int $storeId): array
    {
        $prefix = Mapping::OPTION_TEXT_PREFIX . '_';
        $result = [];

        foreach ($this->getAttributeBuckets($aggregation) as $bucket) {
            $bucketName    = $bucket->getName();
            $attributeCode = $bucketName;
            if (substr($bucketName, 0, strlen($prefix)) === $prefix) {
                $attributeCode = substr($bucketName, strlen($prefix));
            }
            $attributeCode = $this->layeredNavAttributesProvider->getLayeredNavAttributeByFilterField($bucketName) ?? $attributeCode;

            $label = $attributeCode;
            try {
                $attribute      = $this->attributeRepository->get($attributeCode);
                $frontendLabels = array_filter(
                    $attribute->getFrontendLabels(),
                    function ($frontendLabel) use ($storeId) {
                        return $frontendLabel->getStoreId() == $storeId;
                    }
                );
                if (!empty($frontendLabels)) {
                    $label = reset($frontendLabels)->getLabel();
                }
            } catch (\Magento\Framework\Exception\NoSuchEntityException $exception) {
                $label = $attributeCode;
                $attribute = null;
            }

            $hasMore = false;
            $count   = \count($bucket->getValues());
            $options = [];
            foreach ($bucket->getValues() as $value) {
                if ($this->hideBooleanNoValue($attribute, $value)) {
                    --$count;
                    continue;
                }
                $metrics = $value->getMetrics();
                if ($value->getValue() === '__other_docs') {
                    $count += ((int) $metrics['count'] ?? 0) - 1; // -1 because '__other_docs' is counted in.
                    $hasMore = true;
                    continue;
                }

                $optionLabel = $value->getValue();
                if ($attribute && $attribute->getFrontendInput() == 'boolean') {
                    $optionLabel = (string) $attribute->getSource()->getOptionText($value->getValue());
                }

                $options[] = $this->layerFormatter->buildItem($optionLabel, $value->getValue(), $metrics['count']);
            }

            if (empty($options)) {
                continue;
            }

            $result[$attributeCode] = $this->layerFormatter->buildLayer($label, $count, $attributeCode);
            $result[$attributeCode]['options']  = $options;
            $result[$attributeCode]['has_more'] = $hasMore;
            $result[$attributeCode]['rel_nofollow'] = (bool) $attribute->getIsDisplayRelNofollow();
            $result[$attributeCode]['frontend_input'] = $attribute->getFrontendInput();

            if ($attributeCode !== 'attribute_set_id' &&
                $attribute->getFacetSortOrder() == ElasticBucketInterface::SORT_ORDER_MANUAL) {
                $items = array_column($result[$attributeCode]['options'], null, 'label');
                $options = $attribute->getFrontend()->getSelectOptions();

                $result[$attributeCode]['options'] = $this->addOptionsData($items, $options);
            }
        }

        return $result;
    }

    /**
     * Check if the value "No" should be hide for boolean attributes.
     *
     * @param AttributeInterface|null $attribute Attribute.
     * @param mixed                   $value     Value.
     *
     * @return bool
     */
    private function hideBooleanNoValue(?AttributeInterface $attribute, $value): bool
    {
        return $attribute != null
        && $attribute->getFrontendInput() == 'boolean'
        && $value->getValue() == \Magento\Eav\Model\Entity\Attribute\Source\Boolean::VALUE_NO
        && in_array($attribute->getAttributeCode(), $this->hideNoValueAttributes);
    }

    /**
     * Resort items according option position defined in admin.
     *
     * @param array $items   Items to be sorted.
     * @param array $options Options of attribute.
     *
     * @return array
     */
    private function addOptionsData(array $items, $options)
    {
        $optionPosition = 0;
        if (!empty($options)) {
            foreach ($options as $option) {
                if (isset($option['label']) && !empty($option['label'])) {
                    $optionLabel = trim((string) $option['label']);
                    $optionPosition++;

                    if (isset($items[$optionLabel])) {
                        $items[$optionLabel]['adminSortIndex'] = $optionPosition;
                        $items[$optionLabel]['value']          = $optionLabel;
                    }
                }
            }

            $items = $this->sortOptionsData($items);
        }

        return $items;
    }

    /**
     * Sort items by adminSortIndex key.
     *
     * @param array $items to be sorted.
     *
     * @return array
     */
    private function sortOptionsData(array $items)
    {
        usort($items, function ($item1, $item2) {
            if (!isset($item1['adminSortIndex']) or !isset($item2['adminSortIndex'])) {
                return 0;
            }

            return $item1['adminSortIndex'] <= $item2['adminSortIndex'] ? -1 : 1;
        });

        return $items;
    }

    /**
     * Get attribute buckets excluding specified bucket names
     *
     * @param AggregationInterface $aggregation Aggregation
     *
     * @return \Generator|BucketInterface[]
     */
    private function getAttributeBuckets(AggregationInterface $aggregation)
    {
        foreach ($aggregation->getBuckets() as $bucket) {
            if (\in_array($bucket->getName(), $this->bucketNameFilter, true)) {
                continue;
            }
            if ($this->isBucketEmpty($bucket)) {
                continue;
            }
            yield $bucket;
        }
    }

    /**
     * Check that bucket contains data
     *
     * @param BucketInterface|null $bucket Bucket
     *
     * @return bool
     */
    private function isBucketEmpty(?BucketInterface $bucket): bool
    {
        return null === $bucket || !$bucket->getValues();
    }
}