src/module-elasticsuite-catalog/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/CategoryData.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\ResourceModel\Product\Indexer\Fulltext\Datasource;
use Magento\Catalog\Api\Data\CategoryAttributeInterface;
use Magento\Catalog\Api\Data\CategoryInterface;
use Magento\Eav\Model\Config;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Store\Model\StoreManagerInterface;
use Smile\ElasticsuiteCatalog\Model\ResourceModel\Eav\Indexer\Indexer;
use Magento\Framework\ObjectManagerInterface;
/**
* Categories data datasource resource model.
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*
* @category Smile
* @package Smile\ElasticsuiteCatalog
* @author Aurelien FOUCRET <aurelien.foucret@smile.fr>
*/
class CategoryData extends Indexer
{
/**
* @var array Local cache for category names
*/
private $categoryNameCache = [];
/**
* @var null|CategoryAttributeInterface
*/
private $categoryNameAttribute = null;
/**
* @var null|CategoryAttributeInterface
*/
private $useNameInSearchAttribute = null;
/**
* @var \Magento\Eav\Model\Config
*/
private $eavConfig = null;
/**
* @var ObjectManagerInterface
*/
private $objectManager;
/**
* CategoryData constructor.
*
* @param ResourceConnection $resource Connection Resource.
* @param StoreManagerInterface $storeManager The store manager.
* @param MetadataPool $metadataPool Metadata Pool.
* @param Config $eavConfig EAV Configuration.
* @param ObjectManagerInterface $objectManager Object manager.
*/
public function __construct(
ResourceConnection $resource,
StoreManagerInterface $storeManager,
MetadataPool $metadataPool,
Config $eavConfig,
ObjectManagerInterface $objectManager
) {
$this->eavConfig = $eavConfig;
$this->objectManager = $objectManager;
parent::__construct($resource, $storeManager, $metadataPool);
}
/**
* Load categories data for a list of product ids and a given store.
*
* @param integer $storeId Store id.
* @param array $productIds Product ids list.
*
* @return array
*/
public function loadCategoryData($storeId, $productIds)
{
$select = $this->getCategoryProductSelect($productIds, $storeId);
$categoryData = $this->getConnection()->fetchAll($select);
$categoryIds = [];
foreach ($categoryData as $categoryDataRow) {
$categoryIds[] = $categoryDataRow['category_id'];
}
$storeCategoryName = $this->loadCategoryNames(array_unique($categoryIds), $storeId);
foreach ($categoryData as &$categoryDataRow) {
$categoryDataRow['name'] = '';
if (isset($storeCategoryName[(int) $categoryDataRow['category_id']])) {
$categoryDataRow['name'] = $storeCategoryName[(int) $categoryDataRow['category_id']];
}
}
return $categoryData;
}
/**
* Prepare indexed data select.
*
* @param array $productIds Product ids.
* @param integer $storeId Store id.
*
* @return \Zend_Db_Select
*/
protected function getCategoryProductSelect($productIds, $storeId)
{
$select = $this->getConnection()->select()
->from(['cpi' => $this->getTable($this->getCategoryProductIndexTable($storeId))])
->where('cpi.store_id = ?', $storeId)
->where('cpi.product_id IN(?)', $productIds);
return $select;
}
/**
* Returns category name attribute
*
* @return \Magento\Eav\Model\Entity\Attribute\AbstractAttribute
*/
protected function getCategoryNameAttribute()
{
$this->categoryNameAttribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Category::ENTITY, 'name');
return $this->categoryNameAttribute;
}
/**
* Returns category "use name in product search" attribute
*
* @return \Magento\Eav\Model\Entity\Attribute\AbstractAttribute
*/
protected function getUseNameInSearchAttribute()
{
$this->useNameInSearchAttribute = $this->eavConfig
->getAttribute(\Magento\Catalog\Model\Category::ENTITY, 'use_name_in_product_search');
return $this->useNameInSearchAttribute;
}
/**
* Access to EAV configuration.
*
* @return \Magento\Eav\Model\Config
*/
protected function getEavConfig()
{
return $this->eavConfig;
}
/**
* Get category product index table name.
*
* @param integer $storeId Store id.
*
* @return string
*/
protected function getCategoryProductIndexTable($storeId)
{
// Init table name as legacy table name.
$indexTable = $this->getTable('catalog_category_product_index');
try {
// Retrieve table name for the current store Id from the TableMaintainer.
$tableMaintainer = $this->objectManager->get(\Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer::class);
$indexTable = $tableMaintainer->getMainTable($storeId);
} catch (\Exception $exception) {
// Occurs in Magento version where TableMaintainer is not implemented. Will default to legacy table.
}
return $indexTable;
}
/**
* Add some categories name into the cache of names of categories.
*
* @param array $categoryIds Ids of the categories to be added to the cache.
* @param int $storeId Store Id
*
* @return array
*/
private function loadCategoryNames($categoryIds, $storeId)
{
$loadCategoryIds = $categoryIds;
if (isset($this->categoryNameCache[$storeId])) {
$loadCategoryIds = array_diff($categoryIds, array_keys($this->categoryNameCache[$storeId]));
}
$loadCategoryIds = array_map('intval', $loadCategoryIds);
$useNameAttribute = $this->getUseNameInSearchAttribute();
if (!empty($loadCategoryIds) && $useNameAttribute && $useNameAttribute->getId()) {
$select = $this->prepareCategoryNameSelect($loadCategoryIds, $storeId);
$entityIdField = $this->getEntityMetaData(CategoryInterface::class)->getIdentifierField();
foreach ($this->getConnection()->fetchAll($select) as $row) {
$categoryId = (int) $row[$entityIdField];
$this->categoryNameCache[$storeId][$categoryId] = '';
if ((bool) $row['use_name']) {
$this->categoryNameCache[$storeId][$categoryId] = $row['name'];
}
}
}
return isset($this->categoryNameCache[$storeId]) ? $this->categoryNameCache[$storeId] : [];
}
/**
* Prepare SQL query to retrieve category names
*
* @param array $loadCategoryIds Category ids to load name for
* @param int $storeId The current store Id
*
* @return \Magento\Framework\DB\Select
*/
private function prepareCategoryNameSelect($loadCategoryIds, $storeId)
{
$rootCategoryId = (int) $this->storeManager->getStore($storeId)->getRootCategoryId();
$this->categoryNameCache[$storeId][$rootCategoryId] = '';
$nameAttr = $this->getCategoryNameAttribute();
$useNameAttr = $this->getUseNameInSearchAttribute();
$entityIdField = $this->getEntityMetaData(CategoryInterface::class)->getIdentifierField();
$linkField = $this->getEntityMetaData(CategoryInterface::class)->getLinkField();
$select = $this->connection->select();
$conditions = [
"cat.{$linkField} = default_value.{$linkField}",
"default_value.store_id=0",
"default_value.attribute_id = " . (int) $nameAttr->getAttributeId(),
];
$joinCondition = new \Zend_Db_Expr(implode(" AND ", $conditions));
$select->from(['cat' => $this->getEntityMetaData(CategoryInterface::class)->getEntityTable()], [$entityIdField])
->joinLeft(['default_value' => $nameAttr->getBackendTable()], $joinCondition, [])
->where("cat.$entityIdField != ?", $rootCategoryId)
->where("cat.$entityIdField IN (?)", $loadCategoryIds);
// Join to check for use_name_in_product_search.
$joinUseNameCond = sprintf(
"default_value.$linkField = use_name_default_value.$linkField" .
" AND use_name_default_value.attribute_id = %d AND use_name_default_value.store_id = %d",
(int) $useNameAttr->getAttributeId(),
0
);
$select->joinLeft(['use_name_default_value' => $useNameAttr->getBackendTable()], $joinUseNameCond, []);
if ($this->storeManager->isSingleStoreMode()) {
$select->columns(['name' => 'default_value.value']);
$select->columns(['use_name' => 'COALESCE(use_name_default_value.value,1)']);
return $select;
}
// Multi store additional join to get scoped name value.
$joinStoreNameCond = sprintf(
"cat.$linkField = store_value.$linkField" .
" AND store_value.attribute_id = %d AND store_value.store_id = %d",
(int) $nameAttr->getAttributeId(),
(int) $storeId
);
$select->joinLeft(['store_value' => $nameAttr->getBackendTable()], $joinStoreNameCond, [])
->columns(['name' => 'COALESCE(store_value.value,default_value.value, "")']);
// Multi store additional join to get scoped "use_name_in_product_search" value.
$joinUseNameStoreCond = sprintf(
"cat.$linkField = use_name_store_value.$linkField" .
" AND use_name_store_value.attribute_id = %d AND use_name_store_value.store_id = %d",
(int) $useNameAttr->getAttributeId(),
(int) $storeId
);
$select->joinLeft(['use_name_store_value' => $useNameAttr->getBackendTable()], $joinUseNameStoreCond, [])
->columns(['use_name' => 'COALESCE(use_name_store_value.value,use_name_default_value.value,1)']);
return $select;
}
}