src/module-elasticsuite-catalog/Model/Product/Indexer/Fulltext/Datasource/AttributeData.php
<?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 Aurelien FOUCRET <aurelien.foucret@smile.fr>
* @copyright 2020 Smile
* @license Open Software License ("OSL") v. 3.0
*/
namespace Smile\ElasticsuiteCatalog\Model\Product\Indexer\Fulltext\Datasource;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Store\Model\ScopeInterface;
use Smile\ElasticsuiteCatalog\Helper\AbstractAttribute as AttributeHelper;
use Smile\ElasticsuiteCatalog\Model\Eav\Indexer\Fulltext\Datasource\AbstractAttributeData;
use Smile\ElasticsuiteCatalog\Model\ResourceModel\Eav\Indexer\Fulltext\Datasource\AbstractAttributeData as ResourceModel;
use Smile\ElasticsuiteCore\Api\Index\DatasourceInterface;
use Smile\ElasticsuiteCore\Api\Index\Mapping\DynamicFieldProviderInterface;
use Smile\ElasticsuiteCore\Index\Mapping\FieldFactory;
/**
* Datasource used to index product attributes.
* This class is also used to generate attribute mapping since it implements DynamicFieldProviderInterface.
*
* @SuppressWarnings(PHPMD.LongVariable)
*
* @category Smile
* @package Smile\ElasticsuiteCatalog
* @author Aurelien FOUCRET <aurelien.foucret@smile.fr>
*/
class AttributeData extends AbstractAttributeData implements DatasourceInterface, DynamicFieldProviderInterface
{
/** @var string */
private const XML_PATH_INDEX_CHILD_PRODUCT_SKU = 'smile_elasticsuite_catalogsearch_settings/catalogsearch/index_child_product_sku';
/**
* Scope configuration
*
* @var ScopeConfigInterface
*/
private $scopeConfig;
/**
* @var array
*/
private $forbiddenChildrenAttributes = [];
/**
* @var boolean
*/
private $isIndexingChildProductSkuEnabled;
/**
* Constructor
*
* @param ResourceModel $resourceModel Resource model.
* @param FieldFactory $fieldFactory Mapping field factory.
* @param AttributeHelper $attributeHelper Attribute helper.
* @param array $indexedBackendModels List of indexed backend models added to the default list.
* @param array $forbiddenChildrenAttributes List of the forbidden children attributes.
* @param ScopeConfigInterface $scopeConfig Scope Config.
*/
public function __construct(
ResourceModel $resourceModel,
FieldFactory $fieldFactory,
AttributeHelper $attributeHelper,
array $indexedBackendModels = [],
array $forbiddenChildrenAttributes = [],
ScopeConfigInterface $scopeConfig = null
) {
parent::__construct($resourceModel, $fieldFactory, $attributeHelper, $indexedBackendModels);
$this->scopeConfig = $scopeConfig;
$this->forbiddenChildrenAttributes = array_values($forbiddenChildrenAttributes);
}
/**
* {@inheritdoc}
*/
public function addData($storeId, array $indexData)
{
$productIds = array_keys($indexData);
$indexData = $this->addAttributeData($storeId, $productIds, $indexData);
$relationsByChildId = $this->resourceModel->loadChildrens($productIds, $storeId);
if (!empty($relationsByChildId)) {
$allChildrenIds = array_keys($relationsByChildId);
$childrenIndexData = $this->addAttributeData($storeId, $allChildrenIds);
foreach ($childrenIndexData as $childrenId => $childrenData) {
$enabled = isset($childrenData['status']) && current($childrenData['status']) == 1;
if ($enabled === false) {
unset($childrenIndexData[$childrenId]);
}
}
foreach ($relationsByChildId as $childId => $relations) {
foreach ($relations as $relation) {
$parentId = (int) $relation['parent_id'];
if (isset($indexData[$parentId]) && isset($childrenIndexData[$childId])) {
$indexData[$parentId]['children_ids'][] = $childId;
$this->addRelationData($indexData[$parentId], $childrenIndexData[$childId], $relation);
$this->addChildData($indexData[$parentId], $childrenIndexData[$childId]);
$this->addChildSku($indexData[$parentId], $relation);
}
}
}
}
return $this->filterCompositeProducts($indexData);
}
/**
* Append attribute data to the index.
*
* @param int $storeId Indexed store id.
* @param array $productIds Indexed product ids.
* @param array $indexData Original indexed data.
*
* @return array
*/
private function addAttributeData($storeId, $productIds, $indexData = [])
{
foreach ($this->attributeIdsByTable as $backendTable => $attributeIds) {
$attributesData = $this->loadAttributesRawData($storeId, $productIds, $backendTable, $attributeIds);
foreach ($attributesData as $row) {
$productId = (int) $row['entity_id'];
$indexValues = $this->attributeHelper->prepareIndexValue($row['attribute_id'], $storeId, $row['value']);
if (!isset($indexData[$productId])) {
$indexData[$productId] = [];
}
$indexData[$productId] += $indexValues;
$this->addIndexedAttribute($indexData[$productId], $row['attribute_code']);
}
}
return $indexData;
}
/**
* Append data of child products to the parent.
*
* @param array $parentData Parent product data.
* @param array $childAttributes Child product attributes data.
*
* @return void
*/
private function addChildData(&$parentData, $childAttributes)
{
$authorizedChildAttributes = $parentData['children_attributes'];
$addedChildAttributesData = array_filter(
$childAttributes,
function ($attributeCode) use ($authorizedChildAttributes) {
return in_array($attributeCode, $authorizedChildAttributes);
},
ARRAY_FILTER_USE_KEY
);
foreach ($addedChildAttributesData as $attributeCode => $value) {
if (!isset($parentData[$attributeCode])) {
$parentData[$attributeCode] = [];
}
$parentData[$attributeCode] = array_values(array_unique(array_merge($parentData[$attributeCode], $value)));
}
}
/**
* Append relation information to the index for composite products.
*
* @param array $parentData Parent product data.
* @param array $childAttributes Child product attributes data.
* @param array $relation Relation data between the child and the parent.
*
* @return void
*/
private function addRelationData(&$parentData, $childAttributes, $relation)
{
$childAttributeCodes = array_keys($childAttributes);
if (!isset($parentData['children_attributes'])) {
$parentData['children_attributes'] = ['indexed_attributes'];
}
$childrenAttributes = array_merge(
$parentData['children_attributes'],
array_diff($childAttributeCodes, $this->forbiddenChildrenAttributes)
);
if (isset($relation['configurable_attributes']) && !empty($relation['configurable_attributes'])) {
$attributesCodes = array_map(
function (int $attributeId) {
if (isset($this->attributesById[$attributeId])) {
return $this->attributesById[$attributeId]->getAttributeCode();
}
},
$relation['configurable_attributes']
);
$parentData['configurable_attributes'] = array_values(
array_unique(
array_merge($attributesCodes, $parentData['configurable_attributes'] ?? [])
)
);
}
$parentData['children_attributes'] = array_values(array_unique($childrenAttributes));
}
/**
* Filter out composite product when no enabled children are attached.
*
* @param array $indexData Indexed data.
*
* @return array
*/
private function filterCompositeProducts($indexData)
{
$compositeProductTypes = $this->resourceModel->getCompositeTypes();
foreach ($indexData as $productId => $productData) {
$isComposite = in_array($productData['type_id'], $compositeProductTypes);
$hasChildren = isset($productData['children_ids']) && !empty($productData['children_ids']);
if ($isComposite && !$hasChildren) {
unset($indexData[$productId]);
}
}
return $indexData;
}
/**
* Append SKU of children product to the parent product index data.
*
* @SuppressWarnings(PHPMD.ElseExpression)
*
* @param array $parentData Parent product data.
* @param array $relation Relation data between the child and the parent.
*/
private function addChildSku(&$parentData, $relation)
{
if (isset($parentData['sku']) && !is_array($parentData['sku'])) {
$parentData['sku'] = [$parentData['sku']];
}
if (!$this->isIndexChildProductSkuEnabled()) {
$parentData['sku'][] = $relation['sku'];
$parentData['sku'] = array_unique($parentData['sku']);
} else {
$parentData['children_skus'][] = $relation['sku'];
$parentData['children_skus'] = array_unique($parentData['children_skus']);
}
}
/**
* Append an indexed attributes to indexed data of a given product.
*
* @param array $productIndexData Product Index data
* @param string $attributeCode The attribute code
*/
private function addIndexedAttribute(&$productIndexData, $attributeCode)
{
if (!isset($productIndexData['indexed_attributes'])) {
$productIndexData['indexed_attributes'] = [];
}
// Data can be missing for this attribute (Eg : due to null value being escaped,
// or this attribute is already included in the array).
if (isset($productIndexData[$attributeCode])
&& !in_array($attributeCode, $productIndexData['indexed_attributes'])
) {
$productIndexData['indexed_attributes'][] = $attributeCode;
}
}
/**
* Is indexing child product SKU in dedicated subfield enabled?
*
* @return bool
*/
private function isIndexChildProductSkuEnabled(): bool
{
if (!isset($this->isIndexingChildProductSkuEnabled)) {
$this->isIndexingChildProductSkuEnabled = (bool) $this->getScopeConfig()->getValue(
self::XML_PATH_INDEX_CHILD_PRODUCT_SKU,
ScopeInterface::SCOPE_STORE
);
}
return $this->isIndexingChildProductSkuEnabled;
}
/**
* Get Scope Config object. It can be null to allow BC.
*
* @return \Magento\Framework\App\Config\ScopeConfigInterface
*/
private function getScopeConfig() : ScopeConfigInterface
{
if (null === $this->scopeConfig) {
$this->scopeConfig = ObjectManager::getInstance()->get(ScopeConfigInterface::class);
}
return $this->scopeConfig;
}
}