Smile-SA/elasticsuite

View on GitHub
src/module-elasticsuite-catalog/Block/Plugin/Adminhtml/Product/Attribute/Edit/Tab/FrontPlugin.php

Summary

Maintainability
D
2 days
Test Coverage
<?php
/**
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Smile ElasticSuite to newer
 * versions in the future.
 *
 * @category  Smile
 * @package   Smile\ElasticsuiteCatalog
 * @author    Romain Ruaud <romain.ruaud@smile.fr>
 * @copyright 2021 Smile
 * @license   Open Software License ("OSL") v. 3.0
 */
namespace Smile\ElasticsuiteCatalog\Block\Plugin\Adminhtml\Product\Attribute\Edit\Tab;

use Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit\Tab\Front;
use Magento\Config\Model\Config\Source\Yesno;
use Magento\CatalogSearch\Model\Source\Weight;
use Magento\Framework\Data\Form;
use Magento\Framework\Registry;
use Magento\Framework\UrlInterface;
use Smile\ElasticsuiteCatalog\Model\Attribute\Source\FilterBooleanLogic;
use Smile\ElasticsuiteCatalog\Model\Attribute\Source\FilterSortOrder;
use Magento\Framework\Data\Form\Element\Fieldset;
use Magento\Catalog\Api\Data\EavAttributeInterface;
use Smile\ElasticsuiteCore\Api\Index\Mapping\FieldInterface;

/**
 * Plugin that happend custom fields dedicated to search configuration
 *
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 *
 * @category Smile
 * @package  Smile\ElasticsuiteCatalog
 * @author   Romain Ruaud <romain.ruaud@smile.fr>
 */
class FrontPlugin
{
    /**
     * @var array
     */
    private $movedFields = [
        'is_searchable' => 'elasticsuite_catalog_attribute_fieldset',
        'is_visible_in_advanced_search' => 'elasticsuite_catalog_attribute_fieldset',
        'is_filterable' => 'elasticsuite_catalog_attribute_navigation_fieldset',
        'is_filterable_in_search' => 'elasticsuite_catalog_attribute_navigation_fieldset',
        'used_for_sort_by' => 'elasticsuite_catalog_attribute_fieldset',
        'search_weight' => 'elasticsuite_catalog_attribute_fieldset',
        'position' => 'elasticsuite_catalog_attribute_navigation_fieldset',
    ];

    /**
     * @var Weight
     */
    private $weightSource;

    /**
     * @var Yesno
     */
    private $booleanSource;

    /**
     * @var Registry
     */
    private $coreRegistry;

    /**
     * @var \Smile\ElasticsuiteCatalog\Model\Attribute\Source\FilterSortOrder
     */
    private $filterSortOrder;

    /**
     * @var FilterBooleanLogic
     */
    private $filterBooleanLogic;

    /**
     * @var UrlInterface
     */
    private $urlBuilder;

    /**
     * Class constructor
     *
     * @param Yesno              $booleanSource      The YesNo source.
     * @param Weight             $weightSource       Weight source.
     * @param Registry           $coreRegistry       Core registry.
     * @param FilterSortOrder    $filterSortOrder    Filter Sort Order.
     * @param FilterBooleanLogic $filterBooleanLogic Filter boolean logic source model.
     * @param UrlInterface       $urlBuilder         Url Builder.
     */
    public function __construct(
        Yesno $booleanSource,
        Weight $weightSource,
        Registry $coreRegistry,
        FilterSortOrder $filterSortOrder,
        FilterBooleanLogic $filterBooleanLogic,
        UrlInterface $urlBuilder
    ) {
        $this->weightSource    = $weightSource;
        $this->booleanSource   = $booleanSource;
        $this->coreRegistry    = $coreRegistry;
        $this->filterSortOrder = $filterSortOrder;
        $this->filterBooleanLogic = $filterBooleanLogic;
        $this->urlBuilder         = $urlBuilder;
    }

    /**
     * Append ES specifics fields into the attribute edit store front tab.
     *
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     *
     * @param Front $subject The StoreFront tab
     * @param Front $result  Result
     * @param Form  $form    The form
     *
     * @return Front
     */
    public function afterSetForm(Front $subject, Front $result, Form $form)
    {
        $searchFieldset            = $this->createSearchFieldset($form, $subject);
        $layeredNavigationFieldset = $this->createLayeredNavigationFieldset($form, $subject);
        $advancedFieldset          = $this->createAdvancedFieldset($form, $subject);

        $this->moveOriginalFields($form);
        $this->addSearchFields($searchFieldset);
        $this->addAutocompleteFields($searchFieldset);
        $this->addFacetFields($layeredNavigationFieldset);
        $this->addSortFields($searchFieldset);
        $this->addRelNofollowFields($layeredNavigationFieldset);
        $this->appendSliderDisplayRelatedFields($form, $subject);

        if ($this->getAttribute()->getAttributeCode() == 'name') {
            $form->getElement('is_searchable')->setDisabled(1);
        }

        if (($this->getAttribute()->getSourceModel() == 'Magento\Eav\Model\Entity\Attribute\Source\Boolean')
            || ($this->getAttribute()->getBackendType() == 'int')
            || ($this->getAttribute()->getFrontendClass() == 'validate-digits')
            || ($this->getAttribute()->getBackendType() == 'decimal' || $this->getAttribute()->getFrontendClass() == 'validate-number')
            || (in_array($this->getAttribute()->getFrontendInput(), ['select', 'multiselect'])
                || $this->getAttribute()->getSourceModel() != '')
        ) {
            $this->addIncludeZeroFalseField($advancedFieldset);
        }

        $this->addDefaultAnalyzer($advancedFieldset);

        if (($this->getAttribute()->getBackendType() == 'varchar')
            || (in_array($this->getAttribute()->getFrontendInput(), ['select', 'multiselect']))
        ) {
            $this->addIsSpannableField($advancedFieldset);
            $this->addDisableNormsField($advancedFieldset);
        }

        $this->appendFieldsDependency($subject);

        return $result;
    }

    /**
     * Return the current edit attribute.
     *
     * @return EavAttributeInterface
     */
    private function getAttribute()
    {
        return $this->coreRegistry->registry('entity_attribute');
    }

    /**
     * Append the "Search Configuration" fieldset to the tab.
     *
     * @param Form  $form    Target form.
     * @param Front $subject Target tab.
     *
     * @return Fieldset
     */
    private function createSearchFieldset(Form $form, Front $subject)
    {
        $fieldset = $form->addFieldset(
            'elasticsuite_catalog_attribute_fieldset',
            [
                'legend'      => __('Search Configuration'),
                'collapsable' => $subject->getRequest()->has('popup'),
            ],
            'front_fieldset'
        );

        $fieldset->addClass('es-esfeature__logo');

        return $fieldset;
    }

    /**
     * Append the "Search Configuration" fieldset to the tab.
     *
     * @param Form  $form    Target form.
     * @param Front $subject Target tab.
     *
     * @return Fieldset
     */
    private function createLayeredNavigationFieldset(Form $form, Front $subject)
    {
        $fieldset = $form->addFieldset(
            'elasticsuite_catalog_attribute_navigation_fieldset',
            [
                'legend'      => __('Layered Navigation Configuration'),
                'collapsable' => $subject->getRequest()->has('popup'),
            ],
            'elasticsuite_catalog_attribute_fieldset'
        );

        $fieldset->addClass('es-esfeature__logo');

        return $fieldset;
    }

    /**
     * Append the "Advanced Search Configuration" fieldset to the tab.
     *
     * @param Form  $form    Target form.
     * @param Front $subject Target tab.
     *
     * @return Fieldset
     */
    private function createAdvancedFieldset(Form $form, Front $subject)
    {
        $fieldset = $form->addFieldset(
            'elasticsuite_catalog_attribute_advanced_fieldset',
            [
                'legend'      => __('Advanced Elasticsuite Configuration'),
                'collapsable' => $subject->getRequest()->has('popup'),
            ],
            'elasticsuite_catalog_attribute_navigation_fieldset'
        );

        $fieldset->addClass('es-esfeature__logo');

        return $fieldset;
    }

    /**
     * Move original fields to the new fieldset.
     *
     * @param Form $form Form
     *
     * @return FrontPlugin
     */
    private function moveOriginalFields(Form $form)
    {
        $originalFieldset = $form->getElement('front_fieldset');

        foreach ($this->movedFields as $elementId => $fieldset) {
            $element = $form->getElement($elementId);
            if ($element) {
                $targetFieldset = $form->getElement($fieldset);
                $originalFieldset->removeField($elementId);
                $targetFieldset->addElement($element);
            }
        }

        $filterableFieldNote = __('Can be used only with catalog input type Text field, Dropdown, Multiple Select and Price.');
        if ($form->getElement('is_filterable')) {
            $form->getElement('is_filterable')->addData(['note' => $filterableFieldNote]);
        }

        if ($form->getElement('is_filterable_in_search')) {
            $form->getElement('is_filterable_in_search')->addData(['note' => $filterableFieldNote]);
        }

        return $this;
    }

    /**
     * Append autocomplete related fields.
     *
     * @param Fieldset $fieldset Target fieldset
     *
     * @return FrontPlugin
     */
    private function addAutocompleteFields(Fieldset $fieldset)
    {
        $fieldset->addField(
            'is_displayed_in_autocomplete',
            'select',
            [
                'name'   => 'is_displayed_in_autocomplete',
                'label'  => __('Display in autocomplete'),
                'values' => $this->booleanSource->toOptionArray(),
            ],
            'is_used_in_spellcheck'
        );

        return $this;
    }

    /**
     * Append faceting related fields.
     *
     * @param Fieldset $fieldset Target fieldset
     *
     * @return FrontPlugin
     */
    private function addFacetFields(Fieldset $fieldset)
    {
        $fieldset->addField(
            'facet_min_coverage_rate',
            'text',
            [
                'name'  => 'facet_min_coverage_rate',
                'label' => __('Facet coverage rate'),
                'class' => 'validate-digits validate-digits-range digits-range-0-100',
                'value' => '90',
                'note'  => __('Ex: Brand facet will be displayed only if 90% of the product have a brand.'),
            ],
            'is_filterable_in_search'
        );

        $fieldset->addField(
            'facet_max_size',
            'text',
            [
                'name'  => 'facet_max_size',
                'label' => __('Facet max. size'),
                'class' => 'validate-digits validate-zero-or-greater',
                'value' => '10',
                'note'  => __('Max number of values returned by a facet query.'),
            ],
            'facet_min_coverage_rate'
        );

        $fieldset->addField(
            'facet_sort_order',
            'select',
            [
                'name'   => 'facet_sort_order',
                'label'  => __('Facet sort order'),
                'values' => $this->filterSortOrder->toOptionArray(),
            ],
            'facet_max_size'
        );

        $booleanLogicOptions    = $this->filterBooleanLogic->toOptionArray();
        $booleanLogicNote       = __(
            // phpcs:ignore Generic.Files.LineLength
            'When several values are selected in a facet/filter, the default behavior is to combine them with a logical OR ("red" OR "blue").'
            . ' But a logical AND can be handy for some attributes ("egg free" AND "gluten free", "waterproof AND lightweight AND warm").'
        );

        $booleanLogicNote       = __($booleanLogicNote);
        $fieldset->addField(
            'facet_boolean_logic',
            'select',
            [
                'name'   => 'facet_boolean_logic',
                'label'  => __('Facet internal logic'),
                'values' => $booleanLogicOptions,
                'note'   => $booleanLogicNote,
            ],
            'facet_sort_order'
        );

        return $this;
    }

    /**
     * Append spellchecking related fields.
     *
     * @param Fieldset $fieldset Target fieldset
     *
     * @return FrontPlugin
     */
    private function addSearchFields(Fieldset $fieldset)
    {
        $fieldset->addField(
            'search_weight',
            'select',
            [
                'name' => 'search_weight',
                'label' => __('Search Weight'),
                'values' => $this->weightSource->getOptions(),
            ],
            'is_searchable'
        );

        $fieldset->addField(
            'is_used_in_spellcheck',
            'select',
            [
                'name'   => 'is_used_in_spellcheck',
                'label'  => __('Used in spellcheck'),
                'values' => $this->booleanSource->toOptionArray(),
            ],
            'search_weight'
        );

        return $this;
    }

    /**
     * Append sorting related fields.
     *
     * @param Fieldset $fieldset Target fieldset
     *
     * @return FrontPlugin
     */
    private function addSortFields(Fieldset $fieldset)
    {
        $sortFieldsOptions = [
            ['value' => '_first', 'label' => __("First")],
            ['value' => '_last',  'label' => __("Last")],
        ];

        $fieldset->addField(
            'sort_order_asc_missing',
            'select',
            [
                'name'   => 'sort_order_asc_missing',
                'label'  => __('Sort products without value when sorting ASC'),
                'values' => $sortFieldsOptions,
                'note'   => __('How the products which are missing values for this attribute should be treated when using it to sort.'),
            ],
            'used_for_sortby'
        );

        $fieldset->addField(
            'sort_order_desc_missing',
            'select',
            [
                'name'   => 'sort_order_desc_missing',
                'label'  => __('Sort products without value when sorting DESC'),
                'values' => $sortFieldsOptions,
                'note'   => __('How the products which are missing values for this attribute should be treated when using it to sort.'),
            ],
            'sort_order_asc_missing'
        );

        return $this;
    }

    /**
     * Append rel tag related fields.
     *
     * @param Fieldset $fieldset Target fieldset
     *
     * @return FrontPlugin
     */
    private function addRelNofollowFields(Fieldset $fieldset)
    {
        $fieldset->addField(
            'is_display_rel_nofollow',
            'select',
            [
                'name'   => 'is_display_rel_nofollow',
                'label'  => __('Add rel="nofollow" to filter links in Layered Navigation'),
                'values' => $this->booleanSource->toOptionArray(),
                'note'   => __(
                    'Adds HTML attribute rel with value "nofollow" to all filter links of current attribute in Layered Navigation.'
                ),
            ],
            'is_filterable'
        );

        return $this;
    }

    /**
     * Append the "Slider Display Configuration" fieldset to the tab.
     *
     * @param Form  $form    Target form.
     * @param Front $subject Target tab.
     *
     * @return Fieldset
     */
    private function createDisplayFieldset(Form $form, Front $subject)
    {
        $fieldset = $form->addFieldset(
            'elasticsuite_catalog_attribute_display_fieldset',
            [
                'legend'      => __('Slider Display Configuration'),
                'collapsable' => $subject->getRequest()->has('popup'),
            ],
            'elasticsuite_catalog_attribute_fieldset'
        );

        return $fieldset;
    }

    /**
     * Append display related fields.
     *
     * @param Fieldset $fieldset Target fieldset
     *
     * @return FrontPlugin
     */
    private function addDisplayFields(Fieldset $fieldset)
    {
        $fieldset->addField(
            'display_pattern',
            'text',
            [
                'name'  => 'display_pattern',
                'label' => __('Display pattern'),
                'note'  => __('A pattern like %s UNIT where %s is the value. Eg : $%s => $20 or %s € => 20 €'),
            ]
        );

        $fieldset->addField(
            'display_precision',
            'text',
            [
                'name'  => 'display_precision',
                'label' => __('Display Precision'),
                'class' => 'validate-digits',
                'value' => '0',
                'note'  => __('The number of digits to use for precision when displaying.'),
            ],
            'display_pattern'
        );

        return $this;
    }

    /**
     * Append slider display related fields
     *
     * @param Form  $form    The form
     * @param Front $subject The StoreFront tab
     *
     * @return FrontPlugin
     */
    private function appendSliderDisplayRelatedFields($form, $subject)
    {
        $attribute          = $this->getAttribute();
        $isAttributeDecimal = $attribute->getBackendType() == 'decimal' || $attribute->getFrontendClass() == 'validate-number';

        if ($isAttributeDecimal && ($attribute->getFrontendInput() !== 'price')) {
            $displayFieldset = $this->createDisplayFieldset($form, $subject);
            $this->addDisplayFields($displayFieldset);
        }

        return $this;
    }

    /**
     * Add field allowing to configure if zero/false values should be indexed or ignored.
     *
     * @param Fieldset $fieldset Target fieldset
     *
     * @return FrontPlugin
     */
    private function addIncludeZeroFalseField(Fieldset $fieldset)
    {
        $includeZeroFalseNote = __(
            // phpcs:ignore Generic.Files.LineLength
            'If set to Yes, zero (integer or numeric attribute) or false (boolean attribute) values will be indexed in the search engine (default is No).'
            . ' Also applies to source model keys/values of Dropdown/Multiple Select attributes.'
        );
        $fieldset->addField(
            'include_zero_false_values',
            'select',
            [
                'name'   => 'include_zero_false_values',
                'label'  => __('Include zero or false values'),
                'values' => $this->booleanSource->toOptionArray(),
                // phpcs:ignore Generic.Files.LineLength
                'note'   => $includeZeroFalseNote,
            ],
            'used_for_sortby'
        );

        return $this;
    }

    /**
     * Add field allowing to configure if a field can be used for span queries.
     *
     * @param Fieldset $fieldset Target fieldset
     *
     * @return FrontPlugin
     */
    private function addIsSpannableField(Fieldset $fieldset)
    {
        $isSpannableNote = __(
        // phpcs:ignore Generic.Files.LineLength
            'Default : No. If set to Yes, the engine will try to match the current query string at the beginning of this string.'
            . ' Eg: when enabled on "name", if a customer search for "red dress", the engine will give an higher score to products having'
            . ' a name beginning by "red dress". This requires the Span Match Boost feature to be enabled.'
        );
        $fieldset->addField(
            'is_spannable',
            'select',
            [
                'name'   => 'is_spannable',
                'label'  => __('Use this field for span queries'),
                'values' => $this->booleanSource->toOptionArray(),
                // phpcs:ignore Generic.Files.LineLength
                'note'   => $isSpannableNote,
            ],
            'default_analyzer'
        );

        return $this;
    }

    /**
     * Add field allowing to configure if zero/false values should be indexed or ignored.
     *
     * @param Fieldset $fieldset Target fieldset
     *
     * @return FrontPlugin
     */
    private function addDisableNormsField(Fieldset $fieldset)
    {
        $disableNormsNote = __(
        // phpcs:ignore Generic.Files.LineLength
            'Default : No. By default, the score of a text match in a field will vary according to the field length.'
            . ' Eg: when searching for "dress", a product named "red dress" will have an higher score than a product named'
            . ' "red dress with long sleeves". You can set this to "Yes" to discard this behavior.'
        );
        $fieldset->addField(
            'norms_disabled',
            'select',
            [
                'name'   => 'norms_disabled',
                'label'  => __('Discard the field length for scoring'),
                'values' => $this->booleanSource->toOptionArray(),
                // phpcs:ignore Generic.Files.LineLength
                'note'   => $disableNormsNote,
            ],
            'is_spannable'
        );

        return $this;
    }

    /**
     * Add field allowing to configure if a field can be used for span queries.
     *
     * @param Fieldset $fieldset Target fieldset
     *
     * @return FrontPlugin
     */
    private function addDefaultAnalyzer(Fieldset $fieldset)
    {
        $link = sprintf(
            '<a href="%s" target="_blank">%s</a>',
            $this->urlBuilder->getUrl('smile_elasticsuite_indices/analysis/index', ['_query' => []]),
            __("Analysis Page")
        );

        $defaultAnalyzerNote = __(
        // phpcs:ignore Generic.Files.LineLength
            'Default : standard. The default analyzer for this field. Should be set to "reference" for SKU-like fields.'
            . ' You can check the %1 screen to view how these analyzers behave.',
            $link
        );

        $config = [
            'name'   => 'default_analyzer',
            'label'  => __('Default Search Analyzer'),
            'values' => [
                ['value' => FieldInterface::ANALYZER_STANDARD, 'label' => __('standard')],
                ['value' => FieldInterface::ANALYZER_REFERENCE, 'label' => __('reference')],
                ['value' => FieldInterface::ANALYZER_EDGE_NGRAM, 'label' => __('standard_edge_ngram')],
            ],
            // phpcs:ignore Generic.Files.LineLength
            'note'   => $defaultAnalyzerNote,
        ];

        if ($this->getAttribute()->getAttributeCode() === "sku") {
            $config['value'] = FieldInterface::ANALYZER_REFERENCE;
        }

        $fieldset->addField(
            'default_analyzer',
            'select',
            $config,
            'is_used_in_spellcheck'
        );

        return $this;
    }

    /**
     * Manage dependency between fields.
     *
     * @param Front $subject The StoreFront tab
     *
     * @return FrontPlugin
     */
    private function appendFieldsDependency($subject)
    {
        /** @var \Magento\Backend\Block\Widget\Form\Element\Dependence $dependencyBlock */
        $dependencyBlock = $subject->getChildBlock('form_after');

        if ($dependencyBlock) {
            $dependencyBlock
                ->addFieldMap('is_displayed_in_autocomplete', 'is_displayed_in_autocomplete')
                ->addFieldMap('is_filterable', 'is_filterable')
                ->addFieldMap('is_filterable_in_search', 'is_filterable_in_search')
                ->addFieldMap('is_searchable', 'is_searchable')
                ->addFieldMap('is_used_in_spellcheck', 'is_used_in_spellcheck')
                ->addFieldMap('used_for_sort_by', 'used_for_sort_by')
                ->addFieldMap('sort_order_asc_missing', 'sort_order_asc_missing')
                ->addFieldMap('sort_order_desc_missing', 'sort_order_desc_missing')
                ->addFieldMap('is_display_rel_nofollow', 'is_display_rel_nofollow')
                ->addFieldMap('is_spannable', 'is_spannable')
                ->addFieldMap('norms_disabled', 'norms_disabled')
                ->addFieldMap('default_analyzer', 'default_analyzer')
                ->addFieldMap('search_weight', 'search_weight')
                ->addFieldDependence('is_displayed_in_autocomplete', 'is_filterable_in_search', '1')
                ->addFieldDependence('is_used_in_spellcheck', 'is_searchable', '1')
                ->addFieldDependence('is_spannable', 'is_searchable', '1')
                ->addFieldDependence('norms_disabled', 'is_searchable', '1')
                ->addFieldDependence('search_weight', 'is_searchable', '1')
                ->addFieldDependence('default_analyzer', 'is_searchable', '1')
                ->addFieldDependence('sort_order_asc_missing', 'used_for_sort_by', '1')
                ->addFieldDependence('sort_order_desc_missing', 'used_for_sort_by', '1')
                ->addFieldDependence('is_display_rel_nofollow', 'is_filterable', '1');
        }

        return $this;
    }
}