![]() Server : Apache System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64 User : corals ( 1002) PHP Version : 7.4.33 Disable Function : exec,passthru,shell_exec,system Directory : /home/corals/cartforge.co/vendor/magento/module-catalog/Model/ResourceModel/Product/ |
<?php /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ declare(strict_types=1); namespace Magento\Catalog\Model\ResourceModel\Product; use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler; use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\CatalogUrlRewrite\Model\Storage\DbStorage; use Magento\Customer\Api\GroupManagementInterface; use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Indexer\DimensionFactory; use Magento\Store\Model\Indexer\WebsiteDimensionProvider; use Magento\Store\Model\Store; use Magento\Catalog\Model\ResourceModel\Category; use Zend_Db_Expr; use Magento\Catalog\Model\ResourceModel\Product\Gallery; /** * Product collection * * @api * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.NumberOfChildren) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection { /** * Alias for index table */ public const INDEX_TABLE_ALIAS = 'price_index'; /** * Alias for main table */ public const MAIN_TABLE_ALIAS = 'e'; /** * @var string */ protected $_idFieldName = 'entity_id'; /** * @var array */ protected $_flatEnabled = []; /** * @var string */ protected $_productWebsiteTable; /** * @var string */ protected $_productCategoryTable; /** * @var bool */ protected $_addUrlRewrite = false; /** * @var int */ protected $_urlRewriteCategory = ''; /** * @var bool */ protected $_addFinalPrice = false; /** * @var array */ protected $_allIdsCache = null; /** * @var bool */ protected $_addTaxPercents = false; /** * @var \Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation */ protected $_productLimitationFilters; /** * Category product count select * * @var \Magento\Framework\DB\Select */ protected $_productCountSelect = null; /** * @var bool */ protected $_isWebsiteFilter = false; /** * Additional field filters, applied in _productLimitationJoinPrice() * * @var array */ protected $_priceDataFieldFilters = []; /** * @var string|null */ protected $_priceExpression; /** * @var string|null */ protected $_additionalPriceExpression; /** * @var float */ protected $_maxPrice; /** * @var float */ protected $_minPrice; /** * @var float */ protected $_priceStandardDeviation; /** * @var int */ protected $_pricesCount = null; /** * Cloned Select after dispatching 'catalog_prepare_price_select' event * * @var \Magento\Framework\DB\Select */ protected $_catalogPreparePriceSelect = null; /** * Catalog product flat * * @var \Magento\Catalog\Model\Indexer\Product\Flat\State */ protected $_catalogProductFlatState = null; /** * Catalog data * * @var \Magento\Framework\Module\Manager */ protected $moduleManager = null; /** * Core store config * * @var \Magento\Framework\App\Config\ScopeConfigInterface */ protected $_scopeConfig; /** * @var \Magento\Customer\Model\Session */ protected $_customerSession; /** * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface */ protected $_localeDate; /** * @var \Magento\Catalog\Model\ResourceModel\Url */ protected $_catalogUrl; /** * @var \Magento\Catalog\Model\Product\OptionFactory */ protected $_productOptionFactory; /** * Catalog resource helper * * @var \Magento\Catalog\Model\ResourceModel\Helper */ protected $_resourceHelper; /** * @var \Magento\Framework\Stdlib\DateTime */ protected $dateTime; /** * @var \Magento\Customer\Api\GroupManagementInterface */ protected $_groupManagement; /** * @var bool */ protected $needToAddWebsiteNamesToResult; /** * @var Gallery */ private $mediaGalleryResource; /** * @var GalleryReadHandler */ private $productGalleryReadHandler; /** * @var MetadataPool */ private $metadataPool; /** * @var bool|string */ private $linkField; /** * @var \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend */ private $backend; /** * @var TableMaintainer */ private $tableMaintainer; /** * @var PriceTableResolver */ private $priceTableResolver; /** * @var DimensionFactory */ private $dimensionFactory; /** * @var \Magento\Framework\DataObject */ private $emptyItem; /** * @var DbStorage */ private $urlFinder; /** * @var Category */ private $categoryResourceModel; /** * Collection constructor * * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\Framework\App\ResourceConnection $resource * @param \Magento\Eav\Model\EntityFactory $eavEntityFactory * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Framework\Validator\UniversalFactory $universalFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory * @param \Magento\Catalog\Model\ResourceModel\Url $catalogUrl * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param GroupManagementInterface $groupManagement * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection * @param ProductLimitationFactory|null $productLimitationFactory * @param MetadataPool|null $metadataPool * @param TableMaintainer|null $tableMaintainer * @param PriceTableResolver|null $priceTableResolver * @param DimensionFactory|null $dimensionFactory * @param Category|null $categoryResourceModel * @param DbStorage|null $urlFinder * @param GalleryReadHandler|null $productGalleryReadHandler * @param Gallery|null $mediaGalleryResource * * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ public function __construct( \Magento\Framework\Data\Collection\EntityFactory $entityFactory, \Psr\Log\LoggerInterface $logger, \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy, \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\Eav\Model\Config $eavConfig, \Magento\Framework\App\ResourceConnection $resource, \Magento\Eav\Model\EntityFactory $eavEntityFactory, \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, \Magento\Framework\Validator\UniversalFactory $universalFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\Module\Manager $moduleManager, \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, \Magento\Catalog\Model\ResourceModel\Url $catalogUrl, \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, \Magento\Customer\Model\Session $customerSession, \Magento\Framework\Stdlib\DateTime $dateTime, GroupManagementInterface $groupManagement, \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, ProductLimitationFactory $productLimitationFactory = null, MetadataPool $metadataPool = null, TableMaintainer $tableMaintainer = null, PriceTableResolver $priceTableResolver = null, DimensionFactory $dimensionFactory = null, Category $categoryResourceModel = null, DbStorage $urlFinder = null, GalleryReadHandler $productGalleryReadHandler = null, Gallery $mediaGalleryResource = null ) { $this->moduleManager = $moduleManager; $this->_catalogProductFlatState = $catalogProductFlatState; $this->_scopeConfig = $scopeConfig; $this->_productOptionFactory = $productOptionFactory; $this->_catalogUrl = $catalogUrl; $this->_localeDate = $localeDate; $this->_customerSession = $customerSession; $this->_resourceHelper = $resourceHelper; $this->dateTime = $dateTime; $this->_groupManagement = $groupManagement; $productLimitationFactory = $productLimitationFactory ?: ObjectManager::getInstance()->get( \Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory::class ); $this->_productLimitationFilters = $productLimitationFactory->create(); $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); parent::__construct( $entityFactory, $logger, $fetchStrategy, $eventManager, $eavConfig, $resource, $eavEntityFactory, $resourceHelper, $universalFactory, $storeManager, $connection ); $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance() ->get(TableMaintainer::class); $this->priceTableResolver = $priceTableResolver ?: ObjectManager::getInstance() ->get(PriceTableResolver::class); $this->dimensionFactory = $dimensionFactory ?: ObjectManager::getInstance()->get(DimensionFactory::class); $this->categoryResourceModel = $categoryResourceModel ?: ObjectManager::getInstance() ->get(Category::class); $this->urlFinder = $urlFinder ?: ObjectManager::getInstance()->get(DbStorage::class); $this->productGalleryReadHandler = $productGalleryReadHandler ?: ObjectManager::getInstance() ->get(GalleryReadHandler::class); $this->mediaGalleryResource = $mediaGalleryResource ?: ObjectManager::getInstance() ->get(Gallery::class); } /** * Get cloned Select after dispatching 'catalog_prepare_price_select' event * * @return \Magento\Framework\DB\Select */ public function getCatalogPreparedSelect() { return $this->_catalogPreparePriceSelect; } /** * Prepare additional price expression sql part * * @param \Magento\Framework\DB\Select $select * @return $this */ protected function _preparePriceExpressionParameters($select) { // prepare response object for event $response = new \Magento\Framework\DataObject(); $response->setAdditionalCalculations([]); $tableAliases = array_keys($select->getPart(\Magento\Framework\DB\Select::FROM)); if (in_array(self::INDEX_TABLE_ALIAS, $tableAliases)) { $table = self::INDEX_TABLE_ALIAS; } else { $table = reset($tableAliases); } // prepare event arguments $eventArgs = [ 'select' => $select, 'table' => $table, 'store_id' => $this->getStoreId(), 'response_object' => $response, ]; $this->_eventManager->dispatch('catalog_prepare_price_select', $eventArgs); $additional = join('', $response->getAdditionalCalculations()); $this->_priceExpression = $table . '.min_price'; $this->_additionalPriceExpression = $additional; $this->_catalogPreparePriceSelect = clone $select; return $this; } /** * Get price expression sql part * * @param \Magento\Framework\DB\Select $select * @return string */ public function getPriceExpression($select) { //@todo: Add caching of price expression $this->_preparePriceExpressionParameters($select); return $this->_priceExpression; } /** * Get additional price expression sql part * * @param \Magento\Framework\DB\Select $select * @return string */ public function getAdditionalPriceExpression($select) { if (null === $this->_additionalPriceExpression) { $this->_preparePriceExpressionParameters($select); } return $this->_additionalPriceExpression; } /** * Get currency rate * * @return float */ public function getCurrencyRate() { return $this->_storeManager->getStore($this->getStoreId())->getCurrentCurrencyRate(); } /** * Retrieve Catalog Product Flat Helper object * * @return \Magento\Catalog\Model\Indexer\Product\Flat\State */ public function getFlatState() { return $this->_catalogProductFlatState; } /** * Retrieve is flat enabled. Return always false if magento run admin. * * @return bool */ public function isEnabledFlat() { if (!isset($this->_flatEnabled[$this->getStoreId()])) { $this->_flatEnabled[$this->getStoreId()] = $this->getFlatState()->isAvailable(); } return $this->_flatEnabled[$this->getStoreId()]; } /** * Initialize resources * * @return void */ protected function _construct() { if ($this->isEnabledFlat()) { $this->_init( \Magento\Catalog\Model\Product::class, \Magento\Catalog\Model\ResourceModel\Product\Flat::class ); } else { $this->_init(\Magento\Catalog\Model\Product::class, \Magento\Catalog\Model\ResourceModel\Product::class); } $this->_initTables(); } /** * Standard resource collection initialization. Needed for child classes. * * @param string $model * @param string $entityModel * @return $this */ protected function _init($model, $entityModel) { if ($this->isEnabledFlat()) { $entityModel = \Magento\Catalog\Model\ResourceModel\Product\Flat::class; } return parent::_init($model, $entityModel); } /** * Define product website and category product tables * * @return void */ protected function _initTables() { $this->_productWebsiteTable = $this->getResource()->getTable('catalog_product_website'); $this->_productCategoryTable = $this->getResource()->getTable('catalog_category_product'); } /** * Prepare static entity fields * * @return $this */ protected function _prepareStaticFields() { if ($this->isEnabledFlat()) { return $this; } return parent::_prepareStaticFields(); } /** * Get collection empty item. Redeclared for specifying id field name without getting resource model inside model. * * @return \Magento\Framework\DataObject */ public function getNewEmptyItem() { if (null === $this->emptyItem) { $this->emptyItem = parent::getNewEmptyItem(); } $object = clone $this->emptyItem; if ($this->isEnabledFlat()) { $object->setIdFieldName($this->getEntity()->getIdFieldName()); } return $object; } /** * Set entity to use for attributes * * @param \Magento\Eav\Model\Entity\AbstractEntity $entity * @return $this */ public function setEntity($entity) { if ($this->isEnabledFlat() && $entity instanceof \Magento\Framework\Model\ResourceModel\Db\AbstractDb) { $this->_entity = $entity; return $this; } return parent::setEntity($entity); } /** * Set Store scope for collection * * @param mixed $store * @return $this */ public function setStore($store) { parent::setStore($store); if ($this->isEnabledFlat()) { $this->getEntity()->setStoreId($this->getStoreId()); } return $this; } /** * Initialize collection select * Redeclared for remove entity_type_id condition * in catalog_product_entity we store just products * * @return $this */ protected function _initSelect() { if ($this->isEnabledFlat()) { $this->getSelect()->from( [self::MAIN_TABLE_ALIAS => $this->getEntity()->getFlatTableName()], null )->columns( ['status' => new Zend_Db_Expr(ProductStatus::STATUS_ENABLED)] ); $this->addAttributeToSelect($this->getResource()->getDefaultAttributes()); if ($this->_catalogProductFlatState->getFlatIndexerHelper()->isAddChildData()) { $this->getSelect()->where('e.is_child=?', 0); $this->addAttributeToSelect(['child_id', 'is_child']); } } else { $this->getSelect()->from([self::MAIN_TABLE_ALIAS => $this->getEntity()->getEntityTable()]); } return $this; } /** * Load attributes into loaded entities * * @param bool $printQuery * @param bool $logQuery * @return $this */ public function _loadAttributes($printQuery = false, $logQuery = false) { if ($this->isEnabledFlat()) { return $this; } return parent::_loadAttributes($printQuery, $logQuery); } /** * Add attribute to entities in collection. If $attribute=='*' select all attributes. * * @param array|string|integer|\Magento\Framework\App\Config\Element $attribute * @param bool|string $joinType * @return $this */ public function addAttributeToSelect($attribute, $joinType = false) { if ($this->isEnabledFlat()) { if (!is_array($attribute)) { $attribute = [$attribute]; } foreach ($attribute as $attributeCode) { if ($attributeCode == '*') { foreach ($this->getEntity()->getAllTableColumns() as $column) { $this->getSelect()->columns('e.' . $column); $this->_selectAttributes[$column] = $column; $this->_staticFields[$column] = $column; } } else { $columns = $this->getEntity()->getAttributeForSelect($attributeCode); if ($columns) { foreach ($columns as $alias => $column) { $this->getSelect()->columns([$alias => 'e.' . $column]); $this->_selectAttributes[$column] = $column; $this->_staticFields[$column] = $column; } } } } return $this; } return parent::addAttributeToSelect($attribute, $joinType); } /** * Processing collection items after loading. Adding url rewrites, minimal prices, final prices, tax percents. * * @return $this */ protected function _afterLoad() { if ($this->_addUrlRewrite) { $this->_addUrlRewrite(); } $this->_prepareUrlDataObject(); $this->prepareStoreId(); if (count($this)) { $this->_eventManager->dispatch('catalog_product_collection_load_after', ['collection' => $this]); } return $this; } /** * Add Store ID to products from collection. * * @return $this * @since 102.0.8 */ protected function prepareStoreId() { if ($this->getStoreId() !== null) { /** @var $item \Magento\Catalog\Model\Product */ foreach ($this->_items as $item) { $item->setStoreId($this->getStoreId()); } } return $this; } /** * Prepare Url Data object * * @return $this */ protected function _prepareUrlDataObject() { $objects = []; /** @var $item \Magento\Catalog\Model\Product */ foreach ($this->_items as $item) { if ($this->getFlag('do_not_use_category_id')) { $item->setDoNotUseCategoryId(true); } if (!$item->isVisibleInSiteVisibility() && $item->getItemStoreId()) { $objects[$item->getEntityId()] = $item->getItemStoreId(); } } if ($objects && $this->hasFlag('url_data_object')) { $objects = $this->_catalogUrl->getRewriteByProductStore($objects); foreach ($this->_items as $item) { if (isset($objects[$item->getEntityId()])) { $object = new \Magento\Framework\DataObject($objects[$item->getEntityId()]); $item->setUrlDataObject($object); } } } return $this; } /** * Add collection filters by identifiers * * @param mixed $productId * @param boolean $exclude * @return $this */ public function addIdFilter($productId, $exclude = false) { if (empty($productId)) { $this->_setIsLoaded(true); return $this; } if (is_array($productId)) { if ($productId) { if ($exclude) { $condition = ['nin' => $productId]; } else { $condition = ['in' => $productId]; } } else { $condition = ''; } } else { if ($exclude) { $condition = ['neq' => $productId]; } else { $condition = $productId; } } $this->addFieldToFilter('entity_id', $condition); return $this; } /** * Adding product website names to result collection. Add for each product websites information. * * @return $this */ public function addWebsiteNamesToResult() { $this->needToAddWebsiteNamesToResult = true; return $this; } /** * @inheritdoc */ public function load($printQuery = false, $logQuery = false) { if ($this->isLoaded()) { return $this; } parent::load($printQuery, $logQuery); if ($this->needToAddWebsiteNamesToResult) { $this->doAddWebsiteNamesToResult(); } return $this; } /** * Process adding product website names to result collection * * @return $this */ protected function doAddWebsiteNamesToResult() { $productWebsites = []; foreach ($this as $product) { $productWebsites[$product->getId()] = []; } if (!empty($productWebsites)) { $select = $this->getConnection()->select()->from( ['product_website' => $this->_productWebsiteTable] )->join( ['website' => $this->getResource()->getTable('store_website')], 'website.website_id = product_website.website_id', ['name'] )->where( 'product_website.product_id IN (?)', array_keys($productWebsites), \Zend_Db::INT_TYPE )->where( 'website.website_id > ?', 0 ); $data = $this->getConnection()->fetchAll($select); foreach ($data as $row) { $productWebsites[$row['product_id']][] = $row['website_id']; } } foreach ($this as $product) { if (isset($productWebsites[$product->getId()])) { $product->setData('websites', $productWebsites[$product->getId()]); $product->setData('website_ids', $productWebsites[$product->getId()]); } } return $this; } /** * Add store availability filter. Include availability product for store website. * * @param null|string|bool|int|Store $store * @return $this */ public function addStoreFilter($store = null) { if ($store === null) { $store = $this->getStoreId(); } $store = $this->_storeManager->getStore($store); if ($store->getId() != Store::DEFAULT_STORE_ID) { $this->setStoreId($store); $this->_productLimitationFilters['store_id'] = $store->getId(); $this->_applyProductLimitations(); } return $this; } /** * Add website filter to collection * * @param null|bool|int|string|array $websites * @return $this */ public function addWebsiteFilter($websites = null) { if (!is_array($websites)) { $websites = [$this->_storeManager->getWebsite($websites)->getId()]; } $this->_productLimitationFilters['website_ids'] = $websites; if ($this->getStoreId() == Store::DEFAULT_STORE_ID) { $this->_productLimitationJoinWebsite(); } else { $this->_applyProductLimitations(); } return $this; } /** * Get filters applied to collection * * @return \Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation */ public function getLimitationFilters() { return $this->_productLimitationFilters; } /** * Specify category filter for product collection * * @param \Magento\Catalog\Model\Category $category * @return $this */ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) { $this->_productLimitationFilters['category_id'] = $category->getId(); if ($category->getIsAnchor()) { unset($this->_productLimitationFilters['category_is_anchor']); } else { $this->_productLimitationFilters['category_is_anchor'] = 1; } if ($this->getStoreId() == Store::DEFAULT_STORE_ID) { $this->_applyZeroStoreProductLimitations(); } else { $this->_applyProductLimitations(); } return $this; } /** * Filter Product by Categories * * @param array $categoriesFilter * @return $this */ public function addCategoriesFilter(array $categoriesFilter) { foreach ($categoriesFilter as $conditionType => $values) { $categorySelect = $this->getConnection()->select()->from( ['cat' => $this->getTable('catalog_category_product')], 'cat.product_id' )->where($this->getConnection()->prepareSqlCondition('cat.category_id', ['in' => $values])); $selectCondition = [ $this->mapConditionType($conditionType) => $categorySelect ]; $this->getSelect()->where($this->getConnection()->prepareSqlCondition('e.entity_id', $selectCondition)); } return $this; } /** * Map equal and not equal conditions to in and not in * * @param string $conditionType * @return mixed */ private function mapConditionType($conditionType) { $conditionsMap = [ 'eq' => 'in', 'neq' => 'nin' ]; return $conditionsMap[$conditionType] ?? $conditionType; } /** * Join minimal price attribute to result * * @return $this */ public function joinMinimalPrice() { $this->addAttributeToSelect('price')->addAttributeToSelect('minimal_price'); return $this; } /** * Retrieve max value by attribute * * @param string $attribute * @return array|null */ public function getMaxAttributeValue($attribute) { $select = clone $this->getSelect(); $attribute = $this->getEntity()->getAttribute($attribute); $attributeCode = $attribute->getAttributeCode(); $tableAlias = $attributeCode . '_max_value'; $fieldAlias = 'max_' . $attributeCode; $condition = 'e.entity_id = ' . $tableAlias . '.entity_id AND ' . $this->_getConditionSql( $tableAlias . '.attribute_id', $attribute->getId() ); $select->join( [$tableAlias => $attribute->getBackend()->getTable()], $condition, [$fieldAlias => new Zend_Db_Expr('MAX(' . $tableAlias . '.value)')] )->group( 'e.entity_type_id' ); $data = $this->getConnection()->fetchRow($select); if (isset($data[$fieldAlias])) { return $data[$fieldAlias]; } return null; } /** * Retrieve ranging product count for arrtibute range * * @param string $attribute * @param int $range * @return array */ public function getAttributeValueCountByRange($attribute, $range) { $select = clone $this->getSelect(); $attribute = $this->getEntity()->getAttribute($attribute); $attributeCode = $attribute->getAttributeCode(); $tableAlias = $attributeCode . '_range_count_value'; $condition = 'e.entity_id = ' . $tableAlias . '.entity_id AND ' . $this->_getConditionSql( $tableAlias . '.attribute_id', $attribute->getId() ); $select->reset(\Magento\Framework\DB\Select::GROUP); $select->join( [$tableAlias => $attribute->getBackend()->getTable()], $condition, [ 'count_' . $attributeCode => new Zend_Db_Expr('COUNT(DISTINCT e.entity_id)'), 'range_' . $attributeCode => new Zend_Db_Expr('CEIL((' . $tableAlias . '.value+0.01)/' . $range . ')') ] )->group( 'range_' . $attributeCode ); $data = $this->getConnection()->fetchAll($select); $res = []; foreach ($data as $row) { $res[$row['range_' . $attributeCode]] = $row['count_' . $attributeCode]; } return $res; } /** * Retrieve product count by some value of attribute * * @param string $attribute * @return array ($value => $count) */ public function getAttributeValueCount($attribute) { $select = clone $this->getSelect(); $attribute = $this->getEntity()->getAttribute($attribute); $attributeCode = $attribute->getAttributeCode(); $tableAlias = $attributeCode . '_value_count'; $select->reset(\Magento\Framework\DB\Select::GROUP); $condition = 'e.entity_id=' . $tableAlias . '.entity_id AND ' . $this->_getConditionSql( $tableAlias . '.attribute_id', $attribute->getId() ); $select->join( [$tableAlias => $attribute->getBackend()->getTable()], $condition, [ 'count_' . $attributeCode => new Zend_Db_Expr('COUNT(DISTINCT e.entity_id)'), 'value_' . $attributeCode => new Zend_Db_Expr($tableAlias . '.value') ] )->group( 'value_' . $attributeCode ); $data = $this->getConnection()->fetchAll($select); $res = []; foreach ($data as $row) { $res[$row['value_' . $attributeCode]] = $row['count_' . $attributeCode]; } return $res; } /** * Return all attribute values as array in form: * array( * [entity_id_1] => array( * [store_id_1] => store_value_1, * [store_id_2] => store_value_2, * ... * [store_id_n] => store_value_n * ), * ... * ) * * @param string $attribute attribute code * @return array */ public function getAllAttributeValues($attribute) { /** @var $select \Magento\Framework\DB\Select */ $select = clone $this->getSelect(); $attribute = $this->getEntity()->getAttribute($attribute); $fieldMainTable = $this->getConnection()->getAutoIncrementField($this->getMainTable()); $fieldJoinTable = $attribute->getEntity()->getLinkField(); $select->reset() ->from( ['cpe' => $this->getMainTable()], ['entity_id'] )->join( ['cpa' => $attribute->getBackend()->getTable()], 'cpe.' . $fieldMainTable . ' = cpa.' . $fieldJoinTable, ['store_id', 'value'] )->where('attribute_id = ?', (int)$attribute->getId()); $data = $this->getConnection()->fetchAll($select); $res = []; foreach ($data as $row) { $res[$row['entity_id']][$row['store_id']] = $row['value']; } return $res; } /** * Get SQL for get record count without left JOINs * * @return \Magento\Framework\DB\Select */ public function getSelectCountSql() { return $this->_getSelectCountSql(); } /** * Get SQL for get record count * * @param Select $select * @param bool $resetLeftJoins * @return Select */ protected function _getSelectCountSql(?Select $select = null, $resetLeftJoins = true) { $this->_renderFilters(); $countSelect = $select === null ? $this->_getClearSelect() : $this->_buildClearSelect($select); $countSelect->columns('COUNT(DISTINCT e.entity_id)'); if ($resetLeftJoins) { $countSelect->resetJoinLeft(); } $this->removeEntityIdentifierFromGroupBy($countSelect); return $countSelect; } /** * Using `entity_id` for `GROUP BY` causes COUNT() return {n} rows of value = 1 instead of 1 row of value {n} * * @param Select $select * @throws \Zend_Db_Select_Exception */ private function removeEntityIdentifierFromGroupBy(Select $select): void { $originalGroupBy = $select->getPart(Select::GROUP); if (!is_array($originalGroupBy)) { return; } $groupBy = array_filter($originalGroupBy, function ($field) { return !$field || false === strpos($field, $this->getIdFieldName()); }); $select->setPart(Select::GROUP, $groupBy); } /** * Prepare statistics data * * @return $this */ protected function _prepareStatisticsData() { $select = clone $this->getSelect(); $priceExpression = $this->getPriceExpression($select) . ' ' . $this->getAdditionalPriceExpression($select); $sqlEndPart = ') * ' . $this->getCurrencyRate() . ', 2)'; $select = $this->_getSelectCountSql($select, false); $select->columns( [ 'max' => 'ROUND(MAX(' . $priceExpression . $sqlEndPart, 'min' => 'ROUND(MIN(' . $priceExpression . $sqlEndPart, 'std' => $this->getConnection()->getStandardDeviationSql('ROUND((' . $priceExpression . $sqlEndPart), ] ); $select->where($this->getPriceExpression($select) . ' IS NOT NULL'); $row = $this->getConnection()->fetchRow($select, $this->_bindParams, \Zend_Db::FETCH_NUM); $this->_pricesCount = (int)$row[0]; $this->_maxPrice = (double)$row[1]; $this->_minPrice = (double)$row[2]; $this->_priceStandardDeviation = (double)$row[3]; return $this; } /** * Retrieve clear select * * @return \Magento\Framework\DB\Select */ protected function _getClearSelect() { return $this->_buildClearSelect(); } /** * Build clear select * * @param \Magento\Framework\DB\Select $select * @return \Magento\Framework\DB\Select */ protected function _buildClearSelect($select = null) { if (null === $select) { $select = clone $this->getSelect(); } $select->reset(\Magento\Framework\DB\Select::ORDER); $select->reset(\Magento\Framework\DB\Select::LIMIT_COUNT); $select->reset(\Magento\Framework\DB\Select::LIMIT_OFFSET); $select->reset(\Magento\Framework\DB\Select::COLUMNS); return $select; } /** * Retrieve all ids for collection * * @param int|string $limit * @param int|string $offset * @return array */ public function getAllIds($limit = null, $offset = null) { $idsSelect = $this->_getClearSelect(); $idsSelect->columns('e.' . $this->getEntity()->getIdFieldName()); $idsSelect->limit($limit, $offset); $idsSelect->resetJoinLeft(); return $this->getConnection()->fetchCol($idsSelect, $this->_bindParams); } /** * Retrieve product count select for categories * * @return \Magento\Framework\DB\Select */ public function getProductCountSelect() { if ($this->_productCountSelect === null) { $this->_productCountSelect = clone $this->getSelect(); $this->_productCountSelect->reset( \Magento\Framework\DB\Select::COLUMNS )->reset( \Magento\Framework\DB\Select::GROUP )->reset( \Magento\Framework\DB\Select::ORDER )->distinct( false )->join( ['count_table' => $this->tableMaintainer->getMainTable($this->getStoreId())], 'count_table.product_id = e.entity_id', [ 'count_table.category_id', 'product_count' => new Zend_Db_Expr('COUNT(DISTINCT count_table.product_id)') ] )->where( 'count_table.store_id = ?', $this->getStoreId() )->group( 'count_table.category_id' ); } return $this->_productCountSelect; } /** * Destruct product count select * * @return $this */ public function unsProductCountSelect() { $this->_productCountSelect = null; return $this; } /** * Adding product count to categories collection * * @param \Magento\Eav\Model\Entity\Collection\AbstractCollection $categoryCollection * @return $this */ public function addCountToCategories($categoryCollection) { $isAnchor = []; $isNotAnchor = []; foreach ($categoryCollection as $category) { if ($category->getIsAnchor()) { $isAnchor[] = $category->getId(); } else { $isNotAnchor[] = $category->getId(); } } $productCounts = []; if ($isAnchor || $isNotAnchor) { $select = $this->getProductCountSelect(); $this->_eventManager->dispatch( 'catalog_product_collection_before_add_count_to_categories', ['collection' => $this] ); if ($isAnchor) { $anchorStmt = clone $select; $anchorStmt->limit(); //reset limits $anchorStmt->where('count_table.category_id IN (?)', $isAnchor, \Zend_Db::INT_TYPE); $productCounts += $this->getConnection()->fetchPairs($anchorStmt); $anchorStmt = null; } if ($isNotAnchor) { $notAnchorStmt = clone $select; $notAnchorStmt->limit(); //reset limits $notAnchorStmt->where('count_table.category_id IN (?)', $isNotAnchor, \Zend_Db::INT_TYPE); $notAnchorStmt->where('count_table.is_parent = 1'); $productCounts += $this->getConnection()->fetchPairs($notAnchorStmt); $notAnchorStmt = null; } $select = null; $this->unsProductCountSelect(); } foreach ($categoryCollection as $category) { $_count = 0; if (isset($productCounts[$category->getId()])) { $_count = $productCounts[$category->getId()]; } $category->setProductCount($_count); } return $this; } /** * Retrieve unique attribute set ids in collection * * @return array */ public function getSetIds() { $select = clone $this->getSelect(); /** @var $select \Magento\Framework\DB\Select */ $select->reset(Select::COLUMNS); $select->reset(Select::ORDER); $select->distinct(true); $select->columns('attribute_set_id'); return $this->getConnection()->fetchCol($select); } /** * Return array of unique product type ids in collection * * @return array */ public function getProductTypeIds() { $select = clone $this->getSelect(); /** @var $select \Magento\Framework\DB\Select */ $select->reset(\Magento\Framework\DB\Select::COLUMNS); $select->distinct(true); $select->columns('type_id'); return $this->getConnection()->fetchCol($select); } /** * Joins url rewrite rules to collection * * @return $this */ public function joinUrlRewrite() { $this->joinTable( 'url_rewrite', 'entity_id = entity_id', ['request_path'], '{{table}}.entity_type = \'' . ProductUrlRewriteGenerator::ENTITY_TYPE . '\'', 'left' ); return $this; } /** * Add URL rewrites data to product. If collection loadded - run processing else set flag. * * @param int|string $categoryId * @return $this */ public function addUrlRewrite($categoryId = '') { $this->_addUrlRewrite = true; $useCategoryUrl = $this->_scopeConfig->getValue( \Magento\Catalog\Helper\Product::XML_PATH_PRODUCT_URL_USE_CATEGORY, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $this->getStoreId() ); if ($useCategoryUrl) { $this->_urlRewriteCategory = $categoryId; } else { $this->_urlRewriteCategory = 0; } if ($this->isLoaded()) { $this->_addUrlRewrite(); } return $this; } /** * Add URL rewrites to collection * * @return void */ protected function _addUrlRewrite() { $productIds = []; foreach ($this->getItems() as $item) { $productIds[] = $item->getEntityId(); } $filter = [ 'entity_type' => 'product', 'entity_id' => $productIds, 'store_id' => $this->getStoreId(), 'is_autogenerated' => 1 ]; if ($this->_urlRewriteCategory) { $filter['metadata']['category_id'] = $this->_urlRewriteCategory; } $rewrites = $this->urlFinder->findAllByData($filter); foreach ($rewrites as $rewrite) { if ($item = $this->getItemById($rewrite->getEntityId())) { $item->setData('request_path', $rewrite->getRequestPath()); } } } /** * Add minimal price data to result * * @return $this */ public function addMinimalPrice() { return $this->addPriceData(); } /** * Add price data for calculate final price * * @return $this */ public function addFinalPrice() { return $this->addPriceData(); } /** * Retrieve all ids * * @param boolean $resetCache * @return array */ public function getAllIdsCache($resetCache = false) { $ids = null; if (!$resetCache) { $ids = $this->_allIdsCache; } if ($ids === null) { $ids = $this->getAllIds(); $this->setAllIdsCache($ids); } return $ids; } /** * Set all ids * * @param array $value * @return $this */ public function setAllIdsCache($value) { $this->_allIdsCache = $value; return $this; } /** * Add Price Data to result * * @param int $customerGroupId * @param int $websiteId * @return $this */ public function addPriceData($customerGroupId = null, $websiteId = null) { $this->_productLimitationFilters->setUsePriceIndex(true); if (!isset($this->_productLimitationFilters['customer_group_id']) && $customerGroupId === null) { $customerGroupId = $this->_customerSession->getCustomerGroupId(); } if (!isset($this->_productLimitationFilters['website_id']) && $websiteId === null) { $websiteId = $this->_storeManager->getStore($this->getStoreId())->getWebsiteId(); } if ($customerGroupId !== null) { $this->_productLimitationFilters['customer_group_id'] = $customerGroupId; } if ($websiteId !== null) { $this->_productLimitationFilters['website_id'] = $websiteId; } $this->_applyProductLimitations(); return $this; } /** * Add attribute to filter * * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute|string|array $attribute * @param array $condition * @param string $joinType * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function addAttributeToFilter($attribute, $condition = null, $joinType = 'inner') { if ($this->isEnabledFlat()) { if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute) { $attribute = $attribute->getAttributeCode(); } if (is_array($attribute)) { $sqlArr = []; foreach ($attribute as $condition) { $sqlArr[] = $this->_getAttributeConditionSql($condition['attribute'], $condition, $joinType); } $conditionSql = '(' . join(') OR (', $sqlArr) . ')'; $this->getSelect()->where($conditionSql); return $this; } if (!isset($this->_selectAttributes[$attribute])) { $this->addAttributeToSelect($attribute); } if (isset($this->_selectAttributes[$attribute])) { $this->getSelect()->where($this->_getConditionSql('e.' . $attribute, $condition)); } return $this; } $this->_allIdsCache = null; if (is_string($attribute) && $attribute == 'is_saleable') { $this->addIsSaleableAttributeToFilter($condition); } elseif (is_string($attribute) && $attribute == 'tier_price') { $this->addTierPriceAttributeToFilter($attribute, $condition); } else { return parent::addAttributeToFilter($attribute, $condition, $joinType); } return $this; } /** * @inheritdoc * * @since 101.0.0 */ protected function getEntityPkName(\Magento\Eav\Model\Entity\AbstractEntity $entity) { return $entity->getLinkField(); } /** * Add require tax percent flag for product collection * * @return $this */ public function addTaxPercents() { $this->_addTaxPercents = true; return $this; } /** * Get require tax percent flag value * * @return bool */ public function requireTaxPercent() { return $this->_addTaxPercents; } /** * Adding product custom options to result collection * * @return $this */ public function addOptionsToResult() { $productsByLinkId = []; foreach ($this as $product) { $productId = $product->getData( $product->getResource()->getLinkField() ); $productsByLinkId[$productId] = $product; } if (!empty($productsByLinkId)) { $options = $this->_productOptionFactory->create()->getCollection()->addTitleToResult( $this->_storeManager->getStore()->getId() )->addPriceToResult( $this->_storeManager->getStore()->getId() )->addProductToFilter( array_keys($productsByLinkId) )->addValuesToResult(); foreach ($options as $option) { if (isset($productsByLinkId[$option->getProductId()])) { $productsByLinkId[$option->getProductId()]->addOption($option); } } } return $this; } /** * Filter products with required options * * @return $this */ public function addFilterByRequiredOptions() { $this->addAttributeToFilter('required_options', [['neq' => 1], ['null' => true]], 'left'); return $this; } /** * Set product visibility filter for enabled products * * @param array $visibility * @return $this */ public function setVisibility($visibility) { $this->_productLimitationFilters['visibility'] = $visibility; if ($this->getStoreId() == Store::DEFAULT_STORE_ID) { $this->addAttributeToFilter('visibility', $visibility); } else { $this->_applyProductLimitations(); } return $this; } /** * Add attribute to sort order * * @param string $attribute * @param string $dir * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ public function addAttributeToSort($attribute, $dir = self::SORT_ORDER_ASC) { if ($attribute == 'position') { if (isset($this->_joinFields[$attribute])) { $this->getSelect()->order($this->_getAttributeFieldName($attribute) . ' ' . $dir); return $this; } if ($this->isEnabledFlat()) { $this->getSelect()->order("cat_index_position {$dir}"); } // optimize if using cat index $filters = $this->_productLimitationFilters; if (isset($filters['category_id']) || isset($filters['visibility'])) { $this->getSelect()->order([ 'cat_index.position ' . $dir, 'e.entity_id ' . \Magento\Framework\DB\Select::SQL_DESC ]); } else { $this->getSelect()->order('e.entity_id ' . $dir); } return $this; } elseif ($attribute == 'is_saleable') { $this->getSelect()->order("is_salable " . $dir); return $this; } $storeId = $this->getStoreId(); if ($attribute == 'price' && $storeId != 0) { $this->addPriceData(); if ($this->_productLimitationFilters->isUsingPriceIndex()) { $this->getSelect()->order("price_index.min_price {$dir}"); return $this; } } if ($this->isEnabledFlat()) { $column = $this->getEntity()->getAttributeSortColumn($attribute); if ($column) { $this->getSelect()->order("e.{$column} {$dir}"); } elseif (isset($this->_joinFields[$attribute])) { $this->getSelect()->order($this->_getAttributeFieldName($attribute) . ' ' . $dir); } return $this; } else { $attrInstance = $this->getEntity()->getAttribute($attribute); if ($attrInstance && $attrInstance->usesSource()) { $attrInstance->getSource()->addValueSortToCollection($this, $dir); return $this; } } return parent::addAttributeToSort($attribute, $dir); } /** * Prepare limitation filters * * @return $this */ protected function _prepareProductLimitationFilters() { if (isset($this->_productLimitationFilters['visibility']) && !isset($this->_productLimitationFilters['store_id'])) { $this->_productLimitationFilters['store_id'] = $this->getStoreId(); } if (isset($this->_productLimitationFilters['category_id']) && !isset($this->_productLimitationFilters['store_id'])) { $this->_productLimitationFilters['store_id'] = $this->getStoreId(); } if (isset($this->_productLimitationFilters['store_id']) && isset($this->_productLimitationFilters['visibility']) && !isset($this->_productLimitationFilters['category_id'])) { $this->_productLimitationFilters['category_id'] = $this->_storeManager->getStore( $this->_productLimitationFilters['store_id'] )->getRootCategoryId(); } return $this; } /** * Join website product limitation * * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _productLimitationJoinWebsite() { $joinWebsite = false; $filters = $this->_productLimitationFilters; $conditions = ['product_website.product_id = e.entity_id']; if (isset($filters['website_ids'])) { $joinWebsite = true; if (count($filters['website_ids']) > 1) { $this->getSelect()->distinct(true); } $conditions[] = $this->getConnection()->quoteInto( 'product_website.website_id IN(?)', $filters['website_ids'], 'int' ); } elseif (isset($filters['store_id']) && !$this->isEnabledFlat() && (!isset($filters['visibility']) && !isset($filters['category_id']))) { $joinWebsite = true; $websiteId = $this->_storeManager->getStore($filters['store_id'])->getWebsiteId(); $conditions[] = $this->getConnection()->quoteInto('product_website.website_id = ?', $websiteId, 'int'); } $fromPart = $this->getSelect()->getPart(\Magento\Framework\DB\Select::FROM); if (isset($fromPart['product_website'])) { if (!$joinWebsite) { unset($fromPart['product_website']); } else { $fromPart['product_website']['joinCondition'] = join(' AND ', $conditions); } $this->getSelect()->setPart(\Magento\Framework\DB\Select::FROM, $fromPart); } elseif ($joinWebsite) { $this->getSelect()->join( ['product_website' => $this->getTable('catalog_product_website')], join(' AND ', $conditions), [] ); } return $this; } /** * Join additional (alternative) store visibility filter * * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function _productLimitationJoinStore() { $filters = $this->_productLimitationFilters; if (!isset($filters['store_table'])) { return $this; } $hasColumn = false; foreach ($this->getSelect()->getPart(\Magento\Framework\DB\Select::COLUMNS) as $columnEntry) { list(, , $alias) = $columnEntry; if ($alias == 'visibility') { $hasColumn = true; } } if (!$hasColumn) { $this->getSelect()->columns('visibility', 'cat_index'); } // Avoid column duplication problems $this->_resourceHelper->prepareColumnsList($this->getSelect()); $whereCond = $this->getConnection()->quoteInto('cat_index.visibility IN(?)', $filters['visibility']); $wherePart = $this->getSelect()->getPart(\Magento\Framework\DB\Select::WHERE); if (array_search('(' . $whereCond . ')', $wherePart) === false) { $this->getSelect()->where($whereCond); } return $this; } /** * Join Product Price Table * * @return $this */ protected function _productLimitationJoinPrice() { return $this->_productLimitationPrice(); } /** * Join Product Price Table with left-join possibility * * @param bool $joinLeft * @return $this * @see \Magento\Catalog\Model\ResourceModel\Product\Collection::_productLimitationJoinPrice() * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _productLimitationPrice($joinLeft = false) { $filters = $this->_productLimitationFilters; if (!$filters->isUsingPriceIndex() || !isset($filters['website_id']) || (string)$filters['website_id'] === '' || !isset($filters['customer_group_id']) || (string)$filters['customer_group_id'] === '' ) { return $this; } // Preventing overriding price loaded from EAV because we want to use the one from index $this->removeAttributeToSelect('price'); $connection = $this->getConnection(); $select = $this->getSelect(); $joinCondArray = []; $joinCondArray[] = 'price_index.entity_id = e.entity_id'; $joinCondArray[] = $connection->quoteInto('price_index.customer_group_id = ?', $filters['customer_group_id']); // Add website condition only if it's different from admin scope if (((int) $filters['website_id']) !== Store::DEFAULT_STORE_ID) { $joinCondArray[] = $connection->quoteInto('price_index.website_id = ?', $filters['website_id']); } $joinCond = join(' AND ', $joinCondArray); $fromPart = $select->getPart(\Magento\Framework\DB\Select::FROM); if (!isset($fromPart['price_index'])) { $least = $connection->getLeastSql(['price_index.min_price', 'price_index.tier_price']); $minimalExpr = $connection->getCheckSql( 'price_index.tier_price IS NOT NULL', $least, 'price_index.min_price' ); $colls = [ 'price', 'tax_class_id', 'final_price', 'minimal_price' => $minimalExpr, 'min_price', 'max_price', 'tier_price', ]; $tableName = [ 'price_index' => $this->priceTableResolver->resolve( 'catalog_product_index_price', [ $this->dimensionFactory->create( CustomerGroupDimensionProvider::DIMENSION_NAME, (string)$filters['customer_group_id'] ), $this->dimensionFactory->create( WebsiteDimensionProvider::DIMENSION_NAME, (string)$filters['website_id'] ) ] ) ]; if ($joinLeft) { $select->joinLeft($tableName, $joinCond, $colls); } else { $select->join($tableName, $joinCond, $colls); } // Set additional field filters foreach ($this->_priceDataFieldFilters as $filterData) { $select->where(sprintf(...$filterData)); } } else { $fromPart['price_index']['joinCondition'] = $joinCond; $select->setPart(\Magento\Framework\DB\Select::FROM, $fromPart); } //Clean duplicated fields $this->_resourceHelper->prepareColumnsList($select); return $this; } /** * Apply front-end price limitation filters to the collection * * @return $this */ public function applyFrontendPriceLimitations() { $this->_productLimitationFilters->setUsePriceIndex(true); if (!isset($this->_productLimitationFilters['customer_group_id'])) { $customerGroupId = $this->_customerSession->getCustomerGroupId(); $this->_productLimitationFilters['customer_group_id'] = $customerGroupId; } if (!isset($this->_productLimitationFilters['website_id'])) { $websiteId = $this->_storeManager->getStore($this->getStoreId())->getWebsiteId(); $this->_productLimitationFilters['website_id'] = $websiteId; } $this->_applyProductLimitations(); return $this; } /** * Apply limitation filters to collection * Method allows using one time category product index table (or product website table) * for different combinations of store_id/category_id/visibility filter states * Method supports multiple changes in one collection object for this parameters * * @return $this */ protected function _applyProductLimitations() { $this->_prepareProductLimitationFilters(); $this->_productLimitationJoinWebsite(); $this->_productLimitationJoinPrice(); $filters = $this->_productLimitationFilters; if (!isset($filters['category_id']) && !isset($filters['visibility'])) { return $this; } $conditions = [ 'cat_index.product_id=e.entity_id', $this->getConnection()->quoteInto('cat_index.store_id=?', $filters['store_id'], 'int'), ]; if (isset($filters['visibility']) && !isset($filters['store_table'])) { $conditions[] = $this->getConnection()->quoteInto( 'cat_index.visibility IN(?)', $filters['visibility'], 'int' ); } $conditions[] = $this->getConnection()->quoteInto('cat_index.category_id=?', $filters['category_id'], 'int'); if (isset($filters['category_is_anchor'])) { $conditions[] = $this->getConnection()->quoteInto('cat_index.is_parent=?', $filters['category_is_anchor']); } $joinCond = join(' AND ', $conditions); $fromPart = $this->getSelect()->getPart(\Magento\Framework\DB\Select::FROM); if (isset($fromPart['cat_index'])) { $fromPart['cat_index']['joinCondition'] = $joinCond; $this->getSelect()->setPart(\Magento\Framework\DB\Select::FROM, $fromPart); } else { $this->getSelect()->join( ['cat_index' => $this->tableMaintainer->getMainTable($this->getStoreId())], $joinCond, ['cat_index_position' => 'position'] ); } $this->_productLimitationJoinStore(); $this->_eventManager->dispatch( 'catalog_product_collection_apply_limitations_after', ['collection' => $this] ); return $this; } /** * Apply limitation filters to collection base on API * Method allows using one time category product table * for combinations of category_id filter states * * @return $this */ protected function _applyZeroStoreProductLimitations() { $filters = $this->_productLimitationFilters; $categories = $this->getChildrenCategories((int)$filters['category_id']); $joinCond = 'cat_pro.product_id = e.entity_id'; $fromPart = $this->getSelect()->getPart(Select::FROM); if (isset($fromPart['cat_pro'])) { $fromPart['cat_pro']['joinCondition'] = $joinCond; $this->getSelect()->setPart(Select::FROM, $fromPart); } else { $conditions = [ $joinCond, $this->getConnection()->quoteInto('cat_pro.category_id IN(?)', $categories, 'int'), ]; $joinCond = join(' AND ', $conditions); $this->getSelect()->join( ['cat_pro' => $this->getTable('catalog_category_product')], $joinCond, ['cat_index_position' => new Zend_Db_Expr('MIN(cat_pro.position)')] )->group('e.entity_id'); } $this->_joinFields['position'] = ['table' => 'cat_pro', 'field' => 'min_position']; return $this; } /** * Get children categories. * * @param int $categoryId * @return array */ private function getChildrenCategories(int $categoryId): array { $categoryIds[] = $categoryId; $anchorCategory = []; $categories = $this->categoryResourceModel->getCategoryWithChildren($categoryId); if (empty($categories)) { return $categoryIds; } $firstCategory = array_shift($categories); if ($firstCategory['is_anchor'] == 1) { //category hierarchy can not be modified by staging updates $entityField = $this->metadataPool->getMetadata(CategoryInterface::class)->getIdentifierField(); $anchorCategory[] = (int)$firstCategory[$entityField]; foreach ($categories as $category) { if (in_array($category['parent_id'], $categoryIds) && in_array($category['parent_id'], $anchorCategory)) { $categoryIds[] = (int)$category[$entityField]; // Storefront approach is to treat non-anchor children of anchor category as anchors. // Adding theirs IDs to $anchorCategory for consistency. if ($category['is_anchor'] == 1 || in_array($category['parent_id'], $anchorCategory)) { $anchorCategory[] = (int)$category[$entityField]; } } } } return $categoryIds; } /** * Add category ids to loaded items * * @return $this */ public function addCategoryIds() { if ($this->getFlag('category_ids_added')) { return $this; } $ids = array_keys($this->_items); if (empty($ids)) { return $this; } $select = $this->getConnection()->select(); $select->from($this->_productCategoryTable, ['product_id', 'category_id']); $select->where('product_id IN (?)', $ids, \Zend_Db::INT_TYPE); $data = $this->getConnection()->fetchAll($select); $categoryIds = []; foreach ($data as $info) { if (isset($categoryIds[$info['product_id']])) { $categoryIds[$info['product_id']][] = $info['category_id']; } else { $categoryIds[$info['product_id']] = [$info['category_id']]; } } foreach ($this->getItems() as $item) { $productId = $item->getId(); if (isset($categoryIds[$productId])) { $item->setCategoryIds($categoryIds[$productId]); } else { $item->setCategoryIds([]); } } $this->setFlag('category_ids_added', true); return $this; } /** * Add tier price data to loaded items. * * @return $this */ public function addTierPriceData() { if ($this->getFlag('tier_price_added')) { return $this; } $productIds = []; foreach ($this->getItems() as $item) { $productIds[] = $item->getData($this->getLinkField()); } if (!$productIds) { return $this; } $select = $this->getTierPriceSelect($productIds); $this->fillTierPriceData($select); $this->setFlag('tier_price_added', true); return $this; } /** * Load collection items filtered by customer group id and add tier price data. * * @param int $customerGroupId * @return $this * @since 102.0.0 */ public function addTierPriceDataByGroupId($customerGroupId) { if ($this->getFlag('tier_price_added')) { return $this; } $productIds = []; foreach ($this->getItems() as $item) { $productIds[] = $item->getData($this->getLinkField()); } if (!$productIds) { return $this; } $select = $this->getTierPriceSelect($productIds); $select->where( '(customer_group_id=? AND all_groups=0) OR all_groups=1', $customerGroupId ); $this->fillTierPriceData($select); $this->setFlag('tier_price_added', true); return $this; } /** * Get tier price select by product ids. * * @param array $productIds * @return \Magento\Framework\DB\Select */ private function getTierPriceSelect(array $productIds) { /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ $attribute = $this->getAttribute('tier_price'); /* @var $backend \Magento\Catalog\Model\Product\Attribute\Backend\Tierprice */ $backend = $attribute->getBackend(); $websiteId = 0; if (!$attribute->isScopeGlobal() && null !== $this->getStoreId()) { $websiteId = $this->_storeManager->getStore($this->getStoreId())->getWebsiteId(); } $select = $backend->getResource()->getSelect($websiteId); $select->columns(['product_id' => $this->getLinkField()])->where( $this->getLinkField() . ' IN(?)', $productIds )->order( 'qty' ); return $select; } /** * Fill tier prices data. * * @param Select $select * @return void */ private function fillTierPriceData(\Magento\Framework\DB\Select $select) { $tierPrices = []; foreach ($this->getConnection()->fetchAll($select) as $row) { $tierPrices[$row['product_id']][] = $row; } foreach ($this->getItems() as $item) { $productId = $item->getData($this->getLinkField()); $this->getBackend()->setPriceData($item, isset($tierPrices[$productId]) ? $tierPrices[$productId] : []); } } /** * Retrieve link field and cache it. * * @return bool|string */ private function getLinkField() { if ($this->linkField === null) { $this->linkField = $this->getConnection()->getAutoIncrementField($this->getTable('catalog_product_entity')); } return $this->linkField; } /** * Retrieve backend model and cache it. * * @return \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend */ private function getBackend() { if ($this->backend === null) { $this->backend = $this->getAttribute('tier_price')->getBackend(); } return $this->backend; } /** * Add field comparison expression * * @param string $comparisonFormat - expression for sprintf() * @param array $fields - list of fields * @return $this * @throws \Exception */ public function addPriceDataFieldFilter($comparisonFormat, $fields) { if (!preg_match('/^%s( (<|>|=|<=|>=|<>) %s)*$/', $comparisonFormat)) { throw new \InvalidArgumentException('Invalid comparison format.'); } if (!is_array($fields)) { $fields = [$fields]; } foreach ($fields as $key => $field) { $fields[$key] = $this->_getMappedField($field); } $this->_priceDataFieldFilters[] = array_merge([$comparisonFormat], $fields); return $this; } /** * Add media gallery data to loaded items * * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @since 101.0.1 * @throws \Magento\Framework\Exception\LocalizedException */ public function addMediaGalleryData() { if ($this->getFlag('media_gallery_added')) { return $this; } if (!$this->getSize()) { return $this; } $items = $this->getItems(); $linkField = $this->getProductEntityMetadata()->getLinkField(); $select = $this->mediaGalleryResource ->createBatchBaseSelect( $this->getStoreId(), $this->getAttribute('media_gallery')->getAttributeId() )->reset( Select::ORDER // we don't care what order is in current scenario )->where( 'entity.' . $linkField . ' IN (?)', array_map( function ($item) use ($linkField) { return (int) $item->getOrigData($linkField); }, $items ) ); $mediaGalleries = []; foreach ($this->getConnection()->fetchAll($select) as $row) { $mediaGalleries[$row[$linkField]][] = $row; } foreach ($items as $item) { $this->productGalleryReadHandler ->addMediaDataToProduct( $item, $mediaGalleries[$item->getOrigData($linkField)] ?? [] ); } $this->setFlag('media_gallery_added', true); return $this; } /** * Get product entity metadata * * @return \Magento\Framework\EntityManager\EntityMetadataInterface * @since 102.0.0 */ public function getProductEntityMetadata() { return $this->metadataPool->getMetadata(ProductInterface::class); } /** * Clear collection * * @return $this */ public function clear() { foreach ($this->_items as $i => $item) { if ($item->hasStockItem()) { $item->unsStockItem(); } $this->_items[$i] = null; } foreach ($this->_itemsById as $i => $item) { $this->_itemsById[$i] = null; } unset($this->_items, $this->_data, $this->_itemsById); $this->_data = []; return parent::clear(); } /** * Set Order field * * @param string $attribute * @param string $dir * @return $this */ public function setOrder($attribute, $dir = Select::SQL_DESC) { if ($attribute == 'price') { $this->addAttributeToSort($attribute, $dir); } else { parent::setOrder($attribute, $dir); } return $this; } /** * Get products max price * * @return float */ public function getMaxPrice() { if ($this->_maxPrice === null) { $this->_prepareStatisticsData(); } return $this->_maxPrice; } /** * Get products min price * * @return float */ public function getMinPrice() { if ($this->_minPrice === null) { $this->_prepareStatisticsData(); } return $this->_minPrice; } /** * Get standard deviation of products price * * @return float */ public function getPriceStandardDeviation() { if ($this->_priceStandardDeviation === null) { $this->_prepareStatisticsData(); } return $this->_priceStandardDeviation; } /** * Get count of product prices * * @return int */ public function getPricesCount() { if ($this->_pricesCount === null) { $this->_prepareStatisticsData(); } return $this->_pricesCount; } /** * Add is_saleable attribute to filter * * @param mixed $condition * @return $this */ private function addIsSaleableAttributeToFilter($condition): self { $columns = $this->getSelect()->getPart(Select::COLUMNS); foreach ($columns as $columnEntry) { list($correlationName, $column, $alias) = $columnEntry; if ($alias == 'is_saleable') { if ($column instanceof Zend_Db_Expr) { $field = $column; } else { $connection = $this->getSelect()->getConnection(); if (empty($correlationName)) { $field = $connection->quoteColumnAs($column, $alias, true); } else { $field = $connection->quoteColumnAs([$correlationName, $column], $alias, true); } } $this->getSelect()->where("{$field} = ?", $condition); break; } } return $this; } /** * Add tier price attribute to filter * * @param string $attribute * @param mixed $condition * @return $this */ private function addTierPriceAttributeToFilter(string $attribute, $condition): self { $attrCode = $attribute; $connection = $this->getConnection(); $attrTable = $this->_getAttributeTableAlias($attrCode); $entity = $this->getEntity(); $fKey = 'e.' . $this->getEntityPkName($entity); $pKey = $attrTable . '.' . $this->getEntityPkName($entity); $attribute = $entity->getAttribute($attrCode); $attrFieldName = $attrTable . '.value'; $fKey = $connection->quoteColumnAs($fKey, null); $pKey = $connection->quoteColumnAs($pKey, null); $condArr = ["{$pKey} = {$fKey}"]; $this->getSelect()->join( [$attrTable => $this->getTable('catalog_product_entity_tier_price')], '(' . implode(') AND (', $condArr) . ')', [$attrCode => $attrFieldName] ); $this->removeAttributeToSelect($attrCode); $this->_filterAttributes[$attrCode] = $attribute->getId(); $this->_joinFields[$attrCode] = ['table' => '', 'field' => $attrFieldName]; $field = $this->_getAttributeTableAlias($attrCode) . '.value'; $conditionSql = $this->_getConditionSql($field, $condition); $this->getSelect()->where($conditionSql, null, Select::TYPE_CONDITION); $this->_totalRecords = null; return $this; } }