Collection.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\CatalogSearch\Model\ResourceModel\Search;
  7. /**
  8. * Search collection
  9. *
  10. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  11. * @api
  12. * @since 100.0.2
  13. */
  14. class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection implements
  15. \Magento\Search\Model\SearchCollectionInterface
  16. {
  17. /**
  18. * Attribute collection
  19. *
  20. * @var array
  21. */
  22. protected $_attributesCollection;
  23. /**
  24. * Search query
  25. *
  26. * @var string
  27. */
  28. protected $_searchQuery;
  29. /**
  30. * Attribute collection factory
  31. *
  32. * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory
  33. */
  34. protected $_attributeCollectionFactory;
  35. /**
  36. * Collection constructor.
  37. * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory
  38. * @param \Psr\Log\LoggerInterface $logger
  39. * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy
  40. * @param \Magento\Framework\Event\ManagerInterface $eventManager
  41. * @param \Magento\Eav\Model\Config $eavConfig
  42. * @param \Magento\Framework\App\ResourceConnection $resource
  43. * @param \Magento\Eav\Model\EntityFactory $eavEntityFactory
  44. * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper
  45. * @param \Magento\Framework\Validator\UniversalFactory $universalFactory
  46. * @param \Magento\Store\Model\StoreManagerInterface $storeManager
  47. * @param \Magento\Framework\Module\Manager $moduleManager
  48. * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState
  49. * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
  50. * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory
  51. * @param \Magento\Catalog\Model\ResourceModel\Url $catalogUrl
  52. * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
  53. * @param \Magento\Customer\Model\Session $customerSession
  54. * @param \Magento\Framework\Stdlib\DateTime $dateTime
  55. * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement
  56. * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $attributeCollectionFactory
  57. * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection
  58. *
  59. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  60. */
  61. public function __construct(
  62. \Magento\Framework\Data\Collection\EntityFactory $entityFactory,
  63. \Psr\Log\LoggerInterface $logger,
  64. \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy,
  65. \Magento\Framework\Event\ManagerInterface $eventManager,
  66. \Magento\Eav\Model\Config $eavConfig,
  67. \Magento\Framework\App\ResourceConnection $resource,
  68. \Magento\Eav\Model\EntityFactory $eavEntityFactory,
  69. \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper,
  70. \Magento\Framework\Validator\UniversalFactory $universalFactory,
  71. \Magento\Store\Model\StoreManagerInterface $storeManager,
  72. \Magento\Framework\Module\Manager $moduleManager,
  73. \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState,
  74. \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
  75. \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory,
  76. \Magento\Catalog\Model\ResourceModel\Url $catalogUrl,
  77. \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
  78. \Magento\Customer\Model\Session $customerSession,
  79. \Magento\Framework\Stdlib\DateTime $dateTime,
  80. \Magento\Customer\Api\GroupManagementInterface $groupManagement,
  81. \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $attributeCollectionFactory,
  82. \Magento\Framework\DB\Adapter\AdapterInterface $connection = null
  83. ) {
  84. $this->_attributeCollectionFactory = $attributeCollectionFactory;
  85. parent::__construct(
  86. $entityFactory,
  87. $logger,
  88. $fetchStrategy,
  89. $eventManager,
  90. $eavConfig,
  91. $resource,
  92. $eavEntityFactory,
  93. $resourceHelper,
  94. $universalFactory,
  95. $storeManager,
  96. $moduleManager,
  97. $catalogProductFlatState,
  98. $scopeConfig,
  99. $productOptionFactory,
  100. $catalogUrl,
  101. $localeDate,
  102. $customerSession,
  103. $dateTime,
  104. $groupManagement,
  105. $connection
  106. );
  107. }
  108. /**
  109. * Add search query filter
  110. *
  111. * @param string $query
  112. * @return $this
  113. */
  114. public function addSearchFilter($query)
  115. {
  116. $this->_searchQuery = $query;
  117. $this->addFieldToFilter(
  118. $this->getEntity()->getLinkField(),
  119. ['in' => new \Zend_Db_Expr($this->_getSearchEntityIdsSql($query))]
  120. );
  121. return $this;
  122. }
  123. /**
  124. * Add backend search query filter (search by all stores)
  125. *
  126. * @param string $query
  127. * @return $this
  128. */
  129. public function addBackendSearchFilter($query)
  130. {
  131. $this->_searchQuery = $query;
  132. $this->addFieldToFilter(
  133. $this->getEntity()->getLinkField(),
  134. ['in' => new \Zend_Db_Expr($this->_getSearchEntityIdsSql($query, false))]
  135. );
  136. return $this;
  137. }
  138. /**
  139. * Retrieve collection of all attributes
  140. *
  141. * @return \Magento\Framework\Data\Collection\AbstractDb
  142. */
  143. protected function _getAttributesCollection()
  144. {
  145. if (!$this->_attributesCollection) {
  146. $this->_attributesCollection = $this->_attributeCollectionFactory->create()->load();
  147. foreach ($this->_attributesCollection as $attribute) {
  148. $attribute->setEntity($this->getEntity());
  149. }
  150. }
  151. return $this->_attributesCollection;
  152. }
  153. /**
  154. * Check attribute is Text and is Searchable
  155. *
  156. * @param \Magento\Catalog\Model\Entity\Attribute $attribute
  157. * @return boolean
  158. */
  159. protected function _isAttributeTextAndSearchable($attribute)
  160. {
  161. if ($attribute->getIsSearchable() && !in_array(
  162. $attribute->getFrontendInput(),
  163. ['select', 'multiselect']
  164. ) && (in_array(
  165. $attribute->getBackendType(),
  166. ['varchar', 'text']
  167. ) || $attribute->getBackendType() == 'static')
  168. ) {
  169. return true;
  170. }
  171. return false;
  172. }
  173. /**
  174. * Check attributes has options and searchable
  175. *
  176. * @param \Magento\Catalog\Model\Entity\Attribute $attribute
  177. * @return boolean
  178. */
  179. protected function _hasAttributeOptionsAndSearchable($attribute)
  180. {
  181. if ($attribute->getIsSearchable() && in_array($attribute->getFrontendInput(), ['select', 'multiselect'])
  182. ) {
  183. return true;
  184. }
  185. return false;
  186. }
  187. /**
  188. * Retrieve SQL for search entities
  189. *
  190. * @param mixed $query
  191. * @param bool $searchOnlyInCurrentStore Search only in current store or in all stores
  192. * @return string
  193. */
  194. protected function _getSearchEntityIdsSql($query, $searchOnlyInCurrentStore = true)
  195. {
  196. $tables = [];
  197. $selects = [];
  198. $likeOptions = ['position' => 'any'];
  199. $linkField = $this->getEntity()->getLinkField();
  200. /**
  201. * Collect tables and attribute ids of attributes with string values
  202. */
  203. foreach ($this->_getAttributesCollection() as $attribute) {
  204. /** @var \Magento\Catalog\Model\Entity\Attribute $attribute */
  205. $attributeCode = $attribute->getAttributeCode();
  206. if ($this->_isAttributeTextAndSearchable($attribute)) {
  207. $table = $attribute->getBackendTable();
  208. if (!isset($tables[$table]) && $attribute->getBackendType() != 'static') {
  209. $tables[$table] = [];
  210. }
  211. if ($attribute->getBackendType() == 'static') {
  212. $selects[] = $this->getConnection()->select()->from(
  213. $table,
  214. $linkField
  215. )->where(
  216. $this->_resourceHelper->getCILike($attributeCode, $this->_searchQuery, $likeOptions)
  217. );
  218. } else {
  219. $tables[$table][] = $attribute->getId();
  220. }
  221. }
  222. }
  223. if ($searchOnlyInCurrentStore) {
  224. $joinCondition = $this->getConnection()->quoteInto(
  225. "t1.{$linkField} = t2.{$linkField} AND t1.attribute_id = t2.attribute_id AND t2.store_id = ?",
  226. $this->getStoreId()
  227. );
  228. } else {
  229. $joinCondition = "t1.{$linkField} = t2.{$linkField} AND t1.attribute_id = t2.attribute_id";
  230. }
  231. $ifValueId = $this->getConnection()->getIfNullSql('t2.value', 't1.value');
  232. foreach ($tables as $table => $attributeIds) {
  233. $selects[] = $this->getConnection()->select()->from(
  234. ['t1' => $table],
  235. $linkField
  236. )->joinLeft(
  237. ['t2' => $table],
  238. $joinCondition,
  239. []
  240. )->where(
  241. 't1.attribute_id IN (?)',
  242. $attributeIds
  243. )->where(
  244. 't1.store_id = ?',
  245. 0
  246. )->where(
  247. $this->_resourceHelper->getCILike($ifValueId, $this->_searchQuery, $likeOptions)
  248. );
  249. }
  250. $sql = $this->_getSearchInOptionSql($query);
  251. if ($sql) {
  252. $selects[] = "SELECT * FROM ({$sql}) AS inoptionsql"; // inherent unions may be inside
  253. }
  254. $sql = $this->getConnection()->select()->union($selects, \Magento\Framework\DB\Select::SQL_UNION_ALL);
  255. return $sql;
  256. }
  257. /**
  258. * Retrieve SQL for search entities by option
  259. *
  260. * @param mixed $query
  261. * @return string
  262. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  263. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  264. */
  265. protected function _getSearchInOptionSql($query)
  266. {
  267. $attributeIds = [];
  268. $attributeTables = [];
  269. $storeId = (int)$this->getStoreId();
  270. /**
  271. * Collect attributes with options
  272. */
  273. foreach ($this->_getAttributesCollection() as $attribute) {
  274. if ($this->_hasAttributeOptionsAndSearchable($attribute)) {
  275. $attributeTables[$attribute->getFrontendInput()] = $attribute->getBackend()->getTable();
  276. $attributeIds[] = $attribute->getId();
  277. }
  278. }
  279. if (empty($attributeIds)) {
  280. return false;
  281. }
  282. $optionTable = $this->_resource->getTableName('eav_attribute_option');
  283. $optionValueTable = $this->_resource->getTableName('eav_attribute_option_value');
  284. $attributesTable = $this->_resource->getTableName('eav_attribute');
  285. /**
  286. * Select option Ids
  287. */
  288. $ifStoreId = $this->getConnection()->getIfNullSql('s.store_id', 'd.store_id');
  289. $ifValue = $this->getConnection()->getCheckSql('s.value_id > 0', 's.value', 'd.value');
  290. $select = $this->getConnection()->select()->from(
  291. ['d' => $optionValueTable],
  292. ['option_id', 'o.attribute_id', 'store_id' => $ifStoreId, 'a.frontend_input']
  293. )->joinLeft(
  294. ['s' => $optionValueTable],
  295. $this->getConnection()->quoteInto('s.option_id = d.option_id AND s.store_id=?', $storeId),
  296. []
  297. )->join(
  298. ['o' => $optionTable],
  299. 'o.option_id=d.option_id',
  300. []
  301. )->join(
  302. ['a' => $attributesTable],
  303. 'o.attribute_id=a.attribute_id',
  304. []
  305. )->where(
  306. 'd.store_id=0'
  307. )->where(
  308. 'o.attribute_id IN (?)',
  309. $attributeIds
  310. )->where(
  311. $this->_resourceHelper->getCILike($ifValue, $this->_searchQuery, ['position' => 'any'])
  312. );
  313. $options = $this->getConnection()->fetchAll($select);
  314. if (empty($options)) {
  315. return false;
  316. }
  317. // build selects of entity ids for specified options ids by frontend input
  318. $selects = [];
  319. foreach (['select' => 'eq', 'multiselect' => 'finset'] as $frontendInput => $condition) {
  320. if (isset($attributeTables[$frontendInput])) {
  321. $where = [];
  322. foreach ($options as $option) {
  323. if ($frontendInput === $option['frontend_input']) {
  324. $findSet = $this->getConnection()->prepareSqlCondition(
  325. 'value',
  326. [$condition => $option['option_id']]
  327. );
  328. $whereCond = "(attribute_id=%d AND store_id=%d AND {$findSet})";
  329. $where[] = sprintf($whereCond, $option['attribute_id'], $option['store_id']);
  330. }
  331. }
  332. if ($where) {
  333. $selects[$frontendInput] = (string)$this->getConnection()->select()->from(
  334. $attributeTables[$frontendInput],
  335. $this->getEntity()->getLinkField()
  336. )->where(
  337. implode(' OR ', $where)
  338. );
  339. }
  340. }
  341. }
  342. $sql = $this->getConnection()->select()->union($selects, \Magento\Framework\DB\Select::SQL_UNION_ALL);
  343. return (string)$sql;
  344. }
  345. }