Select.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\DB;
  7. use Magento\Framework\App\ResourceConnection;
  8. use Magento\Framework\DB\Adapter\AdapterInterface;
  9. /**
  10. * Class for SQL SELECT generation and results.
  11. *
  12. * @api
  13. * @method \Magento\Framework\DB\Select from($name, $cols = '*', $schema = null)
  14. * @method \Magento\Framework\DB\Select join($name, $cond, $cols = '*', $schema = null)
  15. * @method \Magento\Framework\DB\Select joinInner($name, $cond, $cols = '*', $schema = null)
  16. * @method \Magento\Framework\DB\Select joinLeft($name, $cond, $cols = '*', $schema = null)
  17. * @method \Magento\Framework\DB\Select joinNatural($name, $cond, $cols = '*', $schema = null)
  18. * @method \Magento\Framework\DB\Select joinFull($name, $cond, $cols = '*', $schema = null)
  19. * @method \Magento\Framework\DB\Select joinRight($name, $cond, $cols = '*', $schema = null)
  20. * @method \Magento\Framework\DB\Select joinCross($name, $cols = '*', $schema = null)
  21. * @method \Magento\Framework\DB\Select orWhere($cond, $value = null, $type = null)
  22. * @method \Magento\Framework\DB\Select group($spec)
  23. * @method \Magento\Framework\DB\Select order($spec)
  24. * @method \Magento\Framework\DB\Select limitPage($page, $rowCount)
  25. * @method \Magento\Framework\DB\Select forUpdate($flag = true)
  26. * @method \Magento\Framework\DB\Select distinct($flag = true)
  27. * @method \Magento\Framework\DB\Select reset($part = null)
  28. * @method \Magento\Framework\DB\Select columns($cols = '*', $correlationName = null)
  29. * @since 100.0.2
  30. */
  31. class Select extends \Zend_Db_Select
  32. {
  33. /**
  34. * Condition type
  35. */
  36. const TYPE_CONDITION = 'TYPE_CONDITION';
  37. /**
  38. * Straight join key
  39. */
  40. const STRAIGHT_JOIN = 'straightjoin';
  41. /**
  42. * Sql straight join
  43. */
  44. const SQL_STRAIGHT_JOIN = 'STRAIGHT_JOIN';
  45. /**
  46. * @var Select\SelectRenderer
  47. */
  48. private $selectRenderer;
  49. /**
  50. * Class constructor
  51. * Add straight join support
  52. *
  53. * @param Adapter\Pdo\Mysql $adapter
  54. * @param Select\SelectRenderer $selectRenderer
  55. * @param array $parts
  56. */
  57. public function __construct(
  58. \Magento\Framework\DB\Adapter\Pdo\Mysql $adapter,
  59. \Magento\Framework\DB\Select\SelectRenderer $selectRenderer,
  60. $parts = []
  61. ) {
  62. self::$_partsInit = array_merge(self::$_partsInit, $parts);
  63. if (!isset(self::$_partsInit[self::STRAIGHT_JOIN])) {
  64. self::$_partsInit = [self::STRAIGHT_JOIN => false] + self::$_partsInit;
  65. }
  66. $this->selectRenderer = $selectRenderer;
  67. parent::__construct($adapter);
  68. }
  69. /**
  70. * Adds a WHERE condition to the query by AND.
  71. *
  72. * If a value is passed as the second param, it will be quoted
  73. * and replaced into the condition wherever a question-mark
  74. * appears. Array values are quoted and comma-separated.
  75. *
  76. * <code>
  77. * // simplest but non-secure
  78. * $select->where("id = $id");
  79. *
  80. * // secure (ID is quoted but matched anyway)
  81. * $select->where('id = ?', $id);
  82. *
  83. * // alternatively, with named binding
  84. * $select->where('id = :id');
  85. * </code>
  86. *
  87. * Note that it is more correct to use named bindings in your
  88. * queries for values other than strings. When you use named
  89. * bindings, don't forget to pass the values when actually
  90. * making a query:
  91. *
  92. * <code>
  93. * $db->fetchAll($select, array('id' => 5));
  94. * </code>
  95. *
  96. * @param string $cond The WHERE condition.
  97. * @param string $value OPTIONAL A single value to quote into the condition.
  98. * @param string|int|null $type OPTIONAL The type of the given value
  99. * @return \Magento\Framework\DB\Select
  100. */
  101. public function where($cond, $value = null, $type = null)
  102. {
  103. if ($value === null && $type === null) {
  104. $value = '';
  105. } elseif ($type == self::TYPE_CONDITION) {
  106. $type = null;
  107. }
  108. if (is_array($value)) {
  109. $cond = $this->getConnection()->quoteInto($cond, $value);
  110. $value = null;
  111. }
  112. return parent::where($cond, $value, $type);
  113. }
  114. /**
  115. * Reset unused LEFT JOIN(s)
  116. *
  117. * @return $this
  118. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  119. * @SuppressWarnings(PHPMD.NPathComplexity)
  120. */
  121. public function resetJoinLeft()
  122. {
  123. foreach ($this->_parts[self::FROM] as $tableId => $tableProp) {
  124. if ($tableProp['joinType'] == self::LEFT_JOIN) {
  125. $useJoin = false;
  126. foreach ($this->_parts[self::COLUMNS] as $columnEntry) {
  127. list($correlationName, $column) = $columnEntry;
  128. if ($column instanceof \Zend_Db_Expr) {
  129. if ($this->_findTableInCond(
  130. $tableId,
  131. $column
  132. ) || $this->_findTableInCond(
  133. $tableProp['tableName'],
  134. $column
  135. )
  136. ) {
  137. $useJoin = true;
  138. }
  139. } else {
  140. if ($correlationName == $tableId) {
  141. $useJoin = true;
  142. }
  143. }
  144. }
  145. foreach ($this->_parts[self::WHERE] as $where) {
  146. if ($this->_findTableInCond(
  147. $tableId,
  148. $where
  149. ) || $this->_findTableInCond(
  150. $tableProp['tableName'],
  151. $where
  152. )
  153. ) {
  154. $useJoin = true;
  155. }
  156. }
  157. $joinUseInCond = $useJoin;
  158. $joinInTables = [];
  159. foreach ($this->_parts[self::FROM] as $tableCorrelationName => $table) {
  160. if ($tableCorrelationName == $tableId) {
  161. continue;
  162. }
  163. if (!empty($table['joinCondition'])) {
  164. if ($this->_findTableInCond(
  165. $tableId,
  166. $table['joinCondition']
  167. ) || $this->_findTableInCond(
  168. $tableProp['tableName'],
  169. $table['joinCondition']
  170. )
  171. ) {
  172. $useJoin = true;
  173. $joinInTables[] = $tableCorrelationName;
  174. }
  175. }
  176. }
  177. if (!$useJoin) {
  178. unset($this->_parts[self::FROM][$tableId]);
  179. } else {
  180. $this->_parts[self::FROM][$tableId]['useInCond'] = $joinUseInCond;
  181. $this->_parts[self::FROM][$tableId]['joinInTables'] = $joinInTables;
  182. }
  183. }
  184. }
  185. $this->_resetJoinLeft();
  186. return $this;
  187. }
  188. /**
  189. * Validate LEFT joins, and remove it if not exists
  190. *
  191. * @return $this
  192. */
  193. protected function _resetJoinLeft()
  194. {
  195. foreach ($this->_parts[self::FROM] as $tableId => $tableProp) {
  196. if ($tableProp['joinType'] == self::LEFT_JOIN) {
  197. if ($tableProp['useInCond']) {
  198. continue;
  199. }
  200. $used = false;
  201. foreach ($tableProp['joinInTables'] as $table) {
  202. if (isset($this->_parts[self::FROM][$table])) {
  203. $used = true;
  204. break;
  205. }
  206. }
  207. if (!$used) {
  208. unset($this->_parts[self::FROM][$tableId]);
  209. return $this->_resetJoinLeft();
  210. }
  211. }
  212. }
  213. return $this;
  214. }
  215. /**
  216. * Find table name in condition (where, column)
  217. *
  218. * @param string $table
  219. * @param string $cond
  220. * @return bool
  221. */
  222. protected function _findTableInCond($table, $cond)
  223. {
  224. $quote = $this->_adapter->getQuoteIdentifierSymbol();
  225. if (strpos($cond, $quote . $table . $quote . '.') !== false) {
  226. return true;
  227. }
  228. $position = 0;
  229. $result = 0;
  230. $needle = [];
  231. while (is_integer($result)) {
  232. $result = strpos($cond, $table . '.', $position);
  233. if (is_integer($result)) {
  234. $needle[] = $result;
  235. $position = $result + strlen($table) + 1;
  236. }
  237. }
  238. if (!$needle) {
  239. return false;
  240. }
  241. foreach ($needle as $position) {
  242. if ($position == 0) {
  243. return true;
  244. }
  245. if (!preg_match('#[a-z0-9_]#is', substr($cond, $position - 1, 1))) {
  246. return true;
  247. }
  248. }
  249. return false;
  250. }
  251. /**
  252. * Populate the {@link $_parts} 'join' key
  253. *
  254. * Does the dirty work of populating the join key.
  255. *
  256. * The $name and $cols parameters follow the same logic
  257. * as described in the from() method.
  258. *
  259. * @param null|string $type Type of join; inner, left, and null are currently supported
  260. * @param array|string|\Zend_Db_Expr $name Table name
  261. * @param string $cond Join on this condition
  262. * @param array|string $cols The columns to select from the joined table
  263. * @param string $schema The database name to specify, if any.
  264. * @return \Magento\Framework\DB\Select This \Magento\Framework\DB\Select object
  265. * @throws \Zend_Db_Select_Exception
  266. */
  267. protected function _join($type, $name, $cond, $cols, $schema = null)
  268. {
  269. if ($type == self::INNER_JOIN && empty($cond)) {
  270. $type = self::CROSS_JOIN;
  271. }
  272. return parent::_join($type, $name, $cond, $cols, $schema);
  273. }
  274. /**
  275. * Sets a limit count and offset to the query.
  276. *
  277. * @param int $count OPTIONAL The number of rows to return.
  278. * @param int $offset OPTIONAL Start returning after this many rows.
  279. * @return $this
  280. */
  281. public function limit($count = null, $offset = null)
  282. {
  283. if ($count === null) {
  284. $this->reset(self::LIMIT_COUNT);
  285. } else {
  286. $this->_parts[self::LIMIT_COUNT] = (int)$count;
  287. }
  288. if ($offset === null) {
  289. $this->reset(self::LIMIT_OFFSET);
  290. } else {
  291. $this->_parts[self::LIMIT_OFFSET] = (int)$offset;
  292. }
  293. return $this;
  294. }
  295. /**
  296. * Cross Table Update From Current select
  297. *
  298. * @param string|array $table
  299. * @return string
  300. */
  301. public function crossUpdateFromSelect($table)
  302. {
  303. return $this->getConnection()->updateFromSelect($this, $table);
  304. }
  305. /**
  306. * Insert to table from current select
  307. *
  308. * @param string $tableName
  309. * @param array $fields
  310. * @param bool $onDuplicate
  311. * @return string
  312. */
  313. public function insertFromSelect($tableName, $fields = [], $onDuplicate = true)
  314. {
  315. $mode = $onDuplicate ? AdapterInterface::INSERT_ON_DUPLICATE : false;
  316. return $this->getConnection()->insertFromSelect($this, $tableName, $fields, $mode);
  317. }
  318. /**
  319. * Generate INSERT IGNORE query to the table from current select
  320. *
  321. * @param string $tableName
  322. * @param array $fields
  323. * @return string
  324. */
  325. public function insertIgnoreFromSelect($tableName, $fields = [])
  326. {
  327. return $this->getConnection()->insertFromSelect($this, $tableName, $fields, AdapterInterface::INSERT_IGNORE);
  328. }
  329. /**
  330. * Retrieve DELETE query from select
  331. *
  332. * @param string $table The table name or alias
  333. * @return string
  334. */
  335. public function deleteFromSelect($table)
  336. {
  337. return $this->getConnection()->deleteFromSelect($this, $table);
  338. }
  339. /**
  340. * Modify (hack) part of the structured information for the current query
  341. *
  342. * @param string $part
  343. * @param mixed $value
  344. * @return $this
  345. * @throws \Zend_Db_Select_Exception
  346. */
  347. public function setPart($part, $value)
  348. {
  349. $part = strtolower($part);
  350. if (!array_key_exists($part, $this->_parts)) {
  351. throw new \Zend_Db_Select_Exception("Invalid Select part '{$part}'");
  352. }
  353. $this->_parts[$part] = $value;
  354. return $this;
  355. }
  356. /**
  357. * Use a STRAIGHT_JOIN for the SQL Select
  358. *
  359. * @param bool $flag Whether or not the SELECT use STRAIGHT_JOIN (default true).
  360. * @return $this
  361. */
  362. public function useStraightJoin($flag = true)
  363. {
  364. $this->_parts[self::STRAIGHT_JOIN] = (bool)$flag;
  365. return $this;
  366. }
  367. /**
  368. * Render STRAIGHT_JOIN clause
  369. *
  370. * @param string $sql SQL query
  371. * @return string
  372. */
  373. protected function _renderStraightjoin($sql)
  374. {
  375. if ($this->_adapter->supportStraightJoin() && !empty($this->_parts[self::STRAIGHT_JOIN])) {
  376. $sql .= ' ' . self::SQL_STRAIGHT_JOIN;
  377. }
  378. return $sql;
  379. }
  380. /**
  381. * Adds to the internal table-to-column mapping array.
  382. *
  383. * @param string $correlationName The table/join the columns come from.
  384. * @param array|string $cols The list of columns; preferably as an array,
  385. * but possibly as a string containing one column.
  386. * @param bool|string $afterCorrelationName True if it should be prepended,
  387. * a correlation name if it should be inserted
  388. * @return void
  389. */
  390. protected function _tableCols($correlationName, $cols, $afterCorrelationName = null)
  391. {
  392. if (!is_array($cols)) {
  393. $cols = [$cols];
  394. }
  395. foreach ($cols as $k => $v) {
  396. if ($v instanceof Select) {
  397. $cols[$k] = new \Zend_Db_Expr(sprintf('(%s)', $v->assemble()));
  398. }
  399. }
  400. return parent::_tableCols($correlationName, $cols, $afterCorrelationName);
  401. }
  402. /**
  403. * Adds the random order to query
  404. *
  405. * @param string $field integer field name
  406. * @return $this
  407. */
  408. public function orderRand($field = null)
  409. {
  410. $this->_adapter->orderRand($this, $field);
  411. return $this;
  412. }
  413. /**
  414. * Render FOR UPDATE clause
  415. *
  416. * @param string $sql SQL query
  417. * @return string
  418. */
  419. protected function _renderForupdate($sql)
  420. {
  421. if ($this->_parts[self::FOR_UPDATE]) {
  422. $sql = $this->_adapter->forUpdate($sql);
  423. }
  424. return $sql;
  425. }
  426. /**
  427. * Add EXISTS clause
  428. *
  429. * @param Select $select
  430. * @param string $joinCondition
  431. * @param bool $isExists
  432. * @return $this
  433. */
  434. public function exists($select, $joinCondition, $isExists = true)
  435. {
  436. if ($isExists) {
  437. $exists = 'EXISTS (%s)';
  438. } else {
  439. $exists = 'NOT EXISTS (%s)';
  440. }
  441. $select->reset(self::COLUMNS)->columns([new \Zend_Db_Expr('1')])->where($joinCondition);
  442. $exists = sprintf($exists, $select->assemble());
  443. $this->where($exists);
  444. return $this;
  445. }
  446. /**
  447. * Get adapter
  448. *
  449. * @return \Magento\Framework\DB\Adapter\AdapterInterface
  450. */
  451. public function getConnection()
  452. {
  453. return $this->_adapter;
  454. }
  455. /**
  456. * Converts this object to an SQL SELECT string.
  457. *
  458. * @return string|null This object as a SELECT string. (or null if a string cannot be produced.)
  459. * @since 100.1.0
  460. */
  461. public function assemble()
  462. {
  463. return $this->selectRenderer->render($this);
  464. }
  465. /**
  466. * @return string[]
  467. * @since 100.0.11
  468. */
  469. public function __sleep()
  470. {
  471. $properties = array_keys(get_object_vars($this));
  472. $properties = array_diff(
  473. $properties,
  474. [
  475. '_adapter',
  476. 'selectRenderer'
  477. ]
  478. );
  479. return $properties;
  480. }
  481. /**
  482. * Init not serializable fields
  483. *
  484. * @return void
  485. * @since 100.0.11
  486. */
  487. public function __wakeup()
  488. {
  489. $objectManager = \Magento\Framework\App\ObjectManager::getInstance();
  490. $this->_adapter = $objectManager->get(ResourceConnection::class)->getConnection();
  491. $this->selectRenderer = $objectManager->get(\Magento\Framework\DB\Select\SelectRenderer::class);
  492. }
  493. }