Builder.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Rule\Model\Condition\Sql;
  7. use Magento\Framework\App\ObjectManager;
  8. use Magento\Framework\DB\Select;
  9. use Magento\Framework\Exception\NoSuchEntityException;
  10. use Magento\Rule\Model\Condition\AbstractCondition;
  11. use Magento\Rule\Model\Condition\Combine;
  12. use Magento\Eav\Api\AttributeRepositoryInterface;
  13. use Magento\Catalog\Model\Product;
  14. use Magento\Eav\Model\Entity\Collection\AbstractCollection;
  15. /**
  16. * Class SQL Builder
  17. */
  18. class Builder
  19. {
  20. /**
  21. * @var \Magento\Framework\DB\Adapter\AdapterInterface
  22. */
  23. protected $_connection;
  24. /**
  25. * @var array
  26. */
  27. protected $_conditionOperatorMap = [
  28. '==' => ':field = ?',
  29. '!=' => ':field <> ?',
  30. '>=' => ':field >= ?',
  31. '&gt;=' => ':field >= ?',
  32. '>' => ':field > ?',
  33. '&gt;' => ':field > ?',
  34. '<=' => ':field <= ?',
  35. '&lt;=' => ':field <= ?',
  36. '<' => ':field < ?',
  37. '&lt;' => ':field < ?',
  38. '{}' => ':field IN (?)',
  39. '!{}' => ':field NOT IN (?)',
  40. '()' => ':field IN (?)',
  41. '!()' => ':field NOT IN (?)',
  42. ];
  43. /**
  44. * @var array
  45. */
  46. private $stringConditionOperatorMap = [
  47. '{}' => ':field LIKE ?',
  48. '!{}' => ':field NOT LIKE ?',
  49. ];
  50. /**
  51. * @var \Magento\Rule\Model\Condition\Sql\ExpressionFactory
  52. */
  53. protected $_expressionFactory;
  54. /**
  55. * @var AttributeRepositoryInterface
  56. */
  57. private $attributeRepository;
  58. /**
  59. * @param ExpressionFactory $expressionFactory
  60. * @param AttributeRepositoryInterface|null $attributeRepository
  61. */
  62. public function __construct(
  63. ExpressionFactory $expressionFactory,
  64. AttributeRepositoryInterface $attributeRepository = null
  65. ) {
  66. $this->_expressionFactory = $expressionFactory;
  67. $this->attributeRepository = $attributeRepository ?:
  68. ObjectManager::getInstance()->get(AttributeRepositoryInterface::class);
  69. }
  70. /**
  71. * Get tables to join for given conditions combination
  72. *
  73. * @param Combine $combine
  74. * @return array
  75. */
  76. protected function _getCombineTablesToJoin(Combine $combine)
  77. {
  78. $tables = $this->_getChildCombineTablesToJoin($combine);
  79. return $tables;
  80. }
  81. /**
  82. * Get child for given conditions combination
  83. *
  84. * @param Combine $combine
  85. * @param array $tables
  86. * @return array
  87. */
  88. protected function _getChildCombineTablesToJoin(Combine $combine, $tables = [])
  89. {
  90. foreach ($combine->getConditions() as $condition) {
  91. if ($condition->getConditions()) {
  92. $tables = $this->_getChildCombineTablesToJoin($condition);
  93. } else {
  94. /** @var $condition AbstractCondition */
  95. foreach ($condition->getTablesToJoin() as $alias => $table) {
  96. if (!isset($tables[$alias])) {
  97. $tables[$alias] = $table;
  98. }
  99. }
  100. }
  101. }
  102. return $tables;
  103. }
  104. /**
  105. * Join tables from conditions combination to collection
  106. *
  107. * @param AbstractCollection $collection
  108. * @param Combine $combine
  109. * @return $this
  110. */
  111. protected function _joinTablesToCollection(
  112. AbstractCollection $collection,
  113. Combine $combine
  114. ): Builder {
  115. foreach ($this->_getCombineTablesToJoin($combine) as $alias => $joinTable) {
  116. /** @var $condition AbstractCondition */
  117. $collection->getSelect()->joinLeft(
  118. [$alias => $collection->getResource()->getTable($joinTable['name'])],
  119. $joinTable['condition'],
  120. isset($joinTable['columns']) ? $joinTable['columns'] : '*'
  121. );
  122. }
  123. return $this;
  124. }
  125. /**
  126. * Returns sql expression based on rule condition.
  127. *
  128. * @param AbstractCondition $condition
  129. * @param string $value
  130. * @param bool $isDefaultStoreUsed no longer used because caused an issue about not existing table alias
  131. * @return string
  132. * @throws \Magento\Framework\Exception\LocalizedException
  133. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  134. */
  135. protected function _getMappedSqlCondition(
  136. AbstractCondition $condition,
  137. string $value = '',
  138. bool $isDefaultStoreUsed = true
  139. ): string {
  140. $argument = $condition->getMappedSqlField();
  141. // If rule hasn't valid argument - create negative expression to prevent incorrect rule behavior.
  142. if (empty($argument)) {
  143. return $this->_expressionFactory->create(['expression' => '1 = -1']);
  144. }
  145. $conditionOperator = $condition->getOperatorForValidate();
  146. if (!isset($this->_conditionOperatorMap[$conditionOperator])) {
  147. throw new \Magento\Framework\Exception\LocalizedException(__('Unknown condition operator'));
  148. }
  149. $defaultValue = 0;
  150. //operator 'contains {}' is mapped to 'IN()' query that cannot work with substrings
  151. // adding mapping to 'LIKE %%'
  152. if ($condition->getInputType() === 'string'
  153. && in_array($conditionOperator, array_keys($this->stringConditionOperatorMap), true)
  154. ) {
  155. $sql = str_replace(
  156. ':field',
  157. $this->_connection->getIfNullSql($this->_connection->quoteIdentifier($argument), $defaultValue),
  158. $this->stringConditionOperatorMap[$conditionOperator]
  159. );
  160. $bindValue = $condition->getBindArgumentValue();
  161. $expression = $value . $this->_connection->quoteInto($sql, "%$bindValue%");
  162. } else {
  163. $sql = str_replace(
  164. ':field',
  165. $this->_connection->getIfNullSql($this->_connection->quoteIdentifier($argument), $defaultValue),
  166. $this->_conditionOperatorMap[$conditionOperator]
  167. );
  168. $bindValue = $condition->getBindArgumentValue();
  169. $expression = $value . $this->_connection->quoteInto($sql, $bindValue);
  170. }
  171. // values for multiselect attributes can be saved in comma-separated format
  172. // below is a solution for matching such conditions with selected values
  173. if (is_array($bindValue) && \in_array($conditionOperator, ['()', '{}'], true)) {
  174. foreach ($bindValue as $item) {
  175. $expression .= $this->_connection->quoteInto(
  176. " OR (FIND_IN_SET (?, {$this->_connection->quoteIdentifier($argument)}) > 0)",
  177. $item
  178. );
  179. }
  180. }
  181. return $this->_expressionFactory->create(
  182. ['expression' => $expression]
  183. );
  184. }
  185. /**
  186. * Get mapped sql combination.
  187. *
  188. * @param Combine $combine
  189. * @param string $value
  190. * @param bool $isDefaultStoreUsed
  191. * @return string
  192. * @SuppressWarnings(PHPMD.NPathComplexity)
  193. * @throws \Magento\Framework\Exception\LocalizedException
  194. */
  195. protected function _getMappedSqlCombination(
  196. Combine $combine,
  197. string $value = '',
  198. bool $isDefaultStoreUsed = true
  199. ): string {
  200. $out = (!empty($value) ? $value : '');
  201. $value = ($combine->getValue() ? '' : ' NOT ');
  202. $getAggregator = $combine->getAggregator();
  203. $conditions = $combine->getConditions();
  204. foreach ($conditions as $key => $condition) {
  205. /** @var $condition AbstractCondition|Combine */
  206. $con = ($getAggregator == 'any' ? Select::SQL_OR : Select::SQL_AND);
  207. $con = (isset($conditions[$key+1]) ? $con : '');
  208. if ($condition instanceof Combine) {
  209. $out .= $this->_getMappedSqlCombination($condition, $value, $isDefaultStoreUsed);
  210. } else {
  211. $out .= $this->_getMappedSqlCondition($condition, $value, $isDefaultStoreUsed);
  212. }
  213. $out .= $out ? (' ' . $con) : '';
  214. }
  215. return $this->_expressionFactory->create(['expression' => $out]);
  216. }
  217. /**
  218. * Attach conditions filter to collection
  219. *
  220. * @param AbstractCollection $collection
  221. * @param Combine $combine
  222. * @return void
  223. */
  224. public function attachConditionToCollection(
  225. AbstractCollection $collection,
  226. Combine $combine
  227. ): void {
  228. $this->_connection = $collection->getResource()->getConnection();
  229. $this->_joinTablesToCollection($collection, $combine);
  230. $whereExpression = (string)$this->_getMappedSqlCombination($combine);
  231. if (!empty($whereExpression)) {
  232. if (!empty($combine->getConditions())) {
  233. $conditions = '';
  234. $attributeField = '';
  235. foreach ($combine->getConditions() as $condition) {
  236. if ($condition->getData('attribute') === \Magento\Catalog\Api\Data\ProductInterface::SKU) {
  237. $conditions = $condition->getData('value');
  238. $attributeField = $condition->getMappedSqlField();
  239. }
  240. }
  241. $collection->getSelect()->where($whereExpression);
  242. if (!empty($conditions) && !empty($attributeField)) {
  243. $conditions = explode(',', $conditions);
  244. foreach ($conditions as &$condition) {
  245. $condition = "'" . trim($condition) . "'";
  246. }
  247. $conditions = implode(', ', $conditions);
  248. $collection->getSelect()->order("FIELD($attributeField, $conditions)");
  249. }
  250. } else {
  251. // Select ::where method adds braces even on empty expression
  252. $collection->getSelect()->where($whereExpression);
  253. }
  254. }
  255. }
  256. }