PageRenderTime 47ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/library/Gacela/DataSource/Query/Sql.php

http://github.com/noah-goodrich/gacela
PHP | 790 lines | 505 code | 185 blank | 100 comment | 81 complexity | 0fb99ccfa523761e1ba407ae9bd47d43 MD5 | raw file
  1. <?php
  2. /**
  3. * @author noah
  4. * @date 2/24/11
  5. *
  6. *
  7. */
  8. namespace Gacela\DataSource\Query;
  9. class Sql extends Query
  10. {
  11. protected $_operators = array(
  12. 'equals' => '=',
  13. 'notEquals' => '!=',
  14. 'lessThan' => '<',
  15. 'lessThanOrEqualTo' => '<=',
  16. 'greaterThan' => '>',
  17. 'greaterThanOrEqualTo' => '>=',
  18. 'notIn' => 'NOT IN',
  19. 'in' => "IN",
  20. 'like' => 'LIKE',
  21. 'notLike' => 'NOT LIKE',
  22. 'null' => 'IS NULL',
  23. 'notNull' => 'IS NOT NULL'
  24. );
  25. protected $_delete = null;
  26. protected $_from = array();
  27. protected $_groupBy = array();
  28. protected $_having = array();
  29. protected $_insert = array();
  30. protected $_limit = array();
  31. protected $_join = array();
  32. protected $_orderBy = array();
  33. protected $_schema = null;
  34. protected $_select = array();
  35. protected $_sql = null;
  36. protected $_union = array();
  37. protected $_update = array();
  38. protected $_where = array();
  39. protected function _alias($schema)
  40. {
  41. if(is_array($schema)) {
  42. return key($schema);
  43. }
  44. return $schema;
  45. }
  46. protected function _is_function($value)
  47. {
  48. $value = trim($value);
  49. if(strrpos($value, ')') == strlen($value)-1 AND strpos($value, '(') !== false) {
  50. return true;
  51. }
  52. return false;
  53. }
  54. protected function _param($field, $args)
  55. {
  56. return ':'.preg_replace("/[-\.:\$\^\*& ]/", '_', $field).'_'.sha1($args);
  57. }
  58. protected function _from()
  59. {
  60. $_from = array();
  61. foreach($this->_from as $from) {
  62. if(is_array($from[0])) {
  63. if(current($from[0]) instanceof Sql) {
  64. list($table, $args) = current($from[0])->assemble();
  65. $table = "(\n".$table.")";
  66. $this->bind($args);
  67. } else {
  68. $table = $this->_quoteIdentifier(current($from[0]));
  69. }
  70. $alias = $this->_quoteIdentifier(key($from[0]));
  71. $_from[] = $table." AS ".$alias;
  72. } else {
  73. $_from[] = $this->_quoteIdentifier($from[0]);
  74. }
  75. }
  76. return join(', ', $_from)."\n";
  77. }
  78. protected function _group()
  79. {
  80. if(!count($this->_groupBy)) {
  81. return '';
  82. }
  83. $sql = 'GROUP BY ';
  84. $i=0;
  85. foreach($this->_groupBy as $field) {
  86. if ($i) {
  87. $sql .= ',';
  88. }
  89. $sql .= $this->_quoteIdentifier($field);
  90. $i++;
  91. }
  92. if(strlen($sql) > 0) {
  93. $sql .= "\n";
  94. }
  95. return $sql;
  96. }
  97. protected function _insert()
  98. {
  99. if(!isset($this->_insert[0])) {
  100. return '';
  101. }
  102. $name = $this->_insert[0];
  103. $data = $this->_insert[1];
  104. if(!isset($data[0]) || !is_array($data[0])) {
  105. $data = array($data);
  106. }
  107. $tmp = current($data);
  108. if(is_object($tmp)) {
  109. $tmp = (array) $tmp;
  110. }
  111. $keys = array();
  112. foreach($tmp as $key => $val) {
  113. $keys[] = $this->_quoteIdentifier($key);
  114. }
  115. array_walk($this->_insert[2], function(&$val) { $val = strtoupper($val); });
  116. $modifiers = !empty($this->_insert[2]) ? join(' ', $this->_insert[2]) : '';
  117. $sql = "INSERT {$modifiers} INTO `{$name}` (".join(',',$keys).") VALUES\n";
  118. // Dynamically sets up the params to be bound
  119. foreach($data as $index => $row) {
  120. $tuple = array_keys($tmp);
  121. array_walk($tuple, function(&$key, $k, $index) { $key = ':'.$key.$index; }, $index);
  122. $sql .= "(".join(",", $tuple)."),\n";
  123. }
  124. // Removes the trailing comma created above.
  125. $sql = substr($sql, 0, strlen($sql) - 2);
  126. // Binding the params per row
  127. foreach($data as $index => $row) {
  128. foreach($row as $key => $field) {
  129. $this->_binds[':'.$key.$index] = $field;
  130. }
  131. }
  132. return array($sql, $this->_binds);
  133. }
  134. protected function _join()
  135. {
  136. $_join = '';
  137. if(!count($this->_join)) {
  138. return $_join;
  139. }
  140. foreach($this->_join as $join) {
  141. $type = strtoupper($join[3]);
  142. if(is_array($join[0])) {
  143. if(current($join[0]) instanceof Sql) {
  144. list($table, $args) = current($join[0])->assemble();
  145. $table = "(\n".$table.")";
  146. $this->bind($args);
  147. } else {
  148. $table = $this->_quoteIdentifier(current($join[0]));
  149. }
  150. $alias = $this->_quoteIdentifier(key($join[0]));
  151. $join[0] = $table." AS ".$alias;
  152. } else {
  153. $join[0] = $this->_quoteIdentifier($join[0]);
  154. }
  155. if(is_array($join[1])) {
  156. $joins = $join[1];
  157. $on = '';
  158. foreach($joins as $key => $val) {
  159. if(!empty($on)) {
  160. $on .= ' AND ';
  161. }
  162. $on .= $this->_quoteIdentifier($key). ' = ' . $this->_quoteIdentifier($val);
  163. }
  164. } else {
  165. $on = $join[1];
  166. }
  167. if($type == 'STRAIGHT') {
  168. $type = 'STRAIGHT_JOIN';
  169. } else {
  170. $type = $type.' JOIN';
  171. }
  172. $_join .= "{$type} {$join[0]} ON {$on}\n";
  173. }
  174. return $_join;
  175. }
  176. protected function _order()
  177. {
  178. if(!count($this->_orderBy)) {
  179. return '';
  180. }
  181. $sql = 'ORDER BY ';
  182. foreach($this->_orderBy as $field => $dir) {
  183. $sql .= $this->_quoteIdentifier($field).' '.$dir.',';
  184. }
  185. $sql = substr($sql, 0, strlen($sql)-1);
  186. if(strlen($sql) > 0) {
  187. $sql .= "\n";
  188. }
  189. return $sql;
  190. }
  191. protected function _quoteIdentifier($identifier)
  192. {
  193. if(!is_string($identifier)) {
  194. $type = is_array($identifier) ? 'array' : get_class($identifier);
  195. throw new \Exception('Identifier in _quoteIdentifier is a(n) '.$type.'!');
  196. }
  197. if(strpos($identifier, '*') !== false) {
  198. return $identifier;
  199. } elseif($this->_is_function($identifier)) {
  200. return $identifier;
  201. } elseif(strpos($identifier, '.') !== false) {
  202. $identifier = explode('.', $identifier);
  203. foreach($identifier as $key => $value) {
  204. $identifier[$key] = $this->_quoteIdentifier($value);
  205. }
  206. return join('.', $identifier);
  207. } else {
  208. return "`$identifier`";
  209. }
  210. }
  211. protected function _select()
  212. {
  213. $select = array();
  214. foreach($this->_from as $from) {
  215. foreach($from[1] as $alias => $field) {
  216. if(preg_match('#[\.|\(\)]#', $field) === 0) {
  217. $field = $this->_alias($from[0]).'.'.$field;
  218. }
  219. if(is_int($alias)) {
  220. $select[] = $this->_quoteIdentifier($field);
  221. } else {
  222. $select[] = $this->_quoteIdentifier($field).' AS '.$this->_quoteIdentifier($alias);
  223. }
  224. }
  225. }
  226. foreach($this->_join as $join) {
  227. if(count($join[2])) {
  228. foreach($join[2] as $alias => $field) {
  229. if(preg_match('#[\.|\(\)]#', $field) === 0) {
  230. $field = $this->_alias($join[0]).'.'.$field;
  231. }
  232. if(is_int($alias)) {
  233. $select[] = $this->_quoteIdentifier($field);
  234. } else {
  235. $select[] = $this->_quoteIdentifier($field).' AS '.$this->_quoteIdentifier($alias);
  236. }
  237. }
  238. }
  239. }
  240. return join(', ', $select)."\n";
  241. }
  242. protected function _union()
  243. {
  244. if(empty($this->_union)) {
  245. return '';
  246. }
  247. $sql = '';
  248. $binds = array();
  249. foreach($this->_union as $union) {
  250. if(!empty($sql)) {
  251. $sql .= "UNION\n";
  252. }
  253. if($union instanceof Sql) {
  254. list($query, $args) = $union->assemble();
  255. } elseif(is_array($union)) {
  256. $query = $union[0];
  257. $args = $union[1];
  258. } else {
  259. $query = $union;
  260. $args = array();
  261. }
  262. $sql .= $query;
  263. $binds = array_merge($args, $binds);
  264. }
  265. $this->bind($binds);
  266. return $sql;
  267. }
  268. protected function _update()
  269. {
  270. if(!isset($this->_update[0])) {
  271. return array();
  272. }
  273. $name = $this->_update[0];
  274. $data = $this->_update[1];
  275. $update = "UPDATE {$name}";
  276. $set = "SET ";
  277. foreach($data as $key => $val) {
  278. $param = $this->_param($key, $val);
  279. $set .= $this->_quoteIdentifier($key)." = ".$param;
  280. $this->_binds[$param] = $val;
  281. $set .= ",\n";
  282. }
  283. $set = substr($set, 0, strlen($set) - 2);
  284. return array($update, $set);
  285. }
  286. protected function _where_or_having($array)
  287. {
  288. $_where = '';
  289. if(!count($array)) {
  290. return $_where;
  291. }
  292. foreach($array as $where) {
  293. if($where[0] instanceof $this) {
  294. list($where[0], $where[1]) = $where[0]->assemble();
  295. } elseif($where[1] instanceof $this) {
  296. list($query, $args) = $where[1]->assemble();
  297. str_replace(':query', $query, $_where[0]);
  298. $this->bind($args);
  299. }
  300. if(empty($_where)) {
  301. $_where = "({$where[0]})\n";
  302. } else {
  303. // Check for OR statements
  304. if($where[2]) {
  305. $_where .= "OR ({$where[0]})\n";
  306. } else {
  307. $_where .= "AND ({$where[0]})\n";
  308. }
  309. }
  310. if(count($where[1])) {
  311. $this->bind($where[1]);
  312. }
  313. }
  314. return $_where;
  315. }
  316. protected function _buildFromCriteria(\Gacela\Criteria $criteria)
  317. {
  318. foreach($criteria as $stmt) {
  319. $op = $stmt[0];
  320. if($op instanceof \Gacela\Criteria) {
  321. $query = new Sql($op);
  322. $query = $query->assemble();
  323. $this->where($query[0], $query[1], $stmt[3]);
  324. // Move along, nothing more to see here
  325. continue;
  326. }
  327. $field = $stmt[1];
  328. $or = isset($stmt[3]) ? $stmt[3] : false;
  329. if(isset($stmt[2]) && $stmt[2] !== false) {
  330. $args = $stmt[2];
  331. } else {
  332. $args = '';
  333. }
  334. if($op == 'limit') {
  335. $this->limit($field, $args);
  336. // Move on, this one is all done.
  337. continue;
  338. } elseif($op == 'sort') {
  339. $this->orderBy($field, $args);
  340. // Move along, move along
  341. continue;
  342. }
  343. $bind = array();
  344. $toBind = '';
  345. if(isset($args)) {
  346. if(!in_array($op, array('in', 'notIn'))) {
  347. $toBind = $this->_param($field, $args);
  348. if(in_array($op, array('like', 'notLike'))) {
  349. $args = '%'.$args.'%';
  350. }
  351. $bind = array($toBind => $args);
  352. }
  353. }
  354. if(in_array($op, array('in', 'notIn'))) {
  355. $this->in($field, $stmt[2], $op === 'in' ? false : true, $or);
  356. } elseif(in_array($op, array('notNull', 'null'))) {
  357. $this->where("{$field} ".$this->_operators[$stmt[0]], array(), $or);
  358. } else {
  359. $this->where($this->_quoteIdentifier($field).' '.$this->_operators[$op]." {$toBind}", $bind, $or);
  360. }
  361. }
  362. }
  363. public function __get($val)
  364. {
  365. $val = '_'.$val;
  366. return $this->$val;
  367. }
  368. /**
  369. * @return array - String for the query, array of parameters to be bound
  370. */
  371. public function assemble()
  372. {
  373. // First make sure this isn't an insert statement.
  374. // If it is just return the insert statement.
  375. $sql = $this->_insert();
  376. if(!empty($sql)) {
  377. return $sql;
  378. }
  379. // Now it might be an update statement in which case we'll skip select and from
  380. $update = $this->_update();
  381. if(isset($update[0])) {
  382. $sql = $update[0]."\n";
  383. }
  384. // If its not an insert or an update, it might also be a delete
  385. if(!is_null($this->_delete)) {
  386. $sql = "DELETE FROM {$this->_delete}\n";
  387. }
  388. // Now there is another type of query - UNION - that needs to be considered
  389. $sql .= $this->_union();
  390. if(empty($sql)) {
  391. $select = trim($this->_select());
  392. $from = trim($this->_from());
  393. $sql = '';
  394. if(!empty($select)) {
  395. $sql .= "SELECT {$select}\n";
  396. }
  397. if(!empty($from)) {
  398. $sql .= "FROM {$from}\n";
  399. }
  400. }
  401. $sql .= $this->_join();
  402. if(isset($update[1])) {
  403. $sql .= $update[1]."\n";
  404. }
  405. $where = $this->_where_or_having($this->_where);
  406. if(!empty($sql) && !empty($where)) {
  407. $sql .= 'WHERE ';
  408. }
  409. $sql .= $where;
  410. $sql .= $this->_group();
  411. $having = $this->_where_or_having($this->_having);
  412. if($having) {
  413. $sql .= 'HAVING '.$having;
  414. }
  415. $sql .= $this->_order();
  416. if(!empty($this->_limit)) {
  417. $sql .= 'LIMIT '.(int) $this->_limit[0].', '.(int) $this->_limit[1]."\n";
  418. }
  419. $this->_sql = $sql;
  420. return array($this->_sql, $this->_binds);
  421. }
  422. /**
  423. * @param array $binds
  424. * @return Sql
  425. */
  426. public function bind(array $binds)
  427. {
  428. foreach($binds as $key => $val) {
  429. $this->_binds[$key] = $this->_cast($val);
  430. }
  431. return $this;
  432. }
  433. /**
  434. * @param $name
  435. * @return Sql
  436. */
  437. public function delete($name)
  438. {
  439. $this->_delete = $name;
  440. return $this;
  441. }
  442. /**
  443. * @param $tableName
  444. * @param array $columns
  445. * @return Sql
  446. */
  447. public function from($tableName, array $columns = array())
  448. {
  449. if(is_array($tableName)) {
  450. $name = key($tableName);
  451. } else {
  452. $name = $tableName;
  453. }
  454. if(empty($columns)) {
  455. $columns = array('*');
  456. }
  457. $this->_from[$name] = array($tableName, $columns);
  458. return $this;
  459. }
  460. /**
  461. * @param string $column
  462. * @return Sql
  463. */
  464. public function groupBy($column)
  465. {
  466. if(!in_array($column, $this->_groupBy)) {
  467. $this->_groupBy[] = $column;
  468. }
  469. return $this;
  470. }
  471. /**
  472. * @param $stmt
  473. * @param array $value
  474. * @param bool $or
  475. * @return Sql
  476. */
  477. public function having($stmt, $value = array(), $or = false)
  478. {
  479. $this->_having[] = array($stmt, $value, $or);
  480. return $this;
  481. }
  482. /**
  483. * @param $field
  484. * @param array $values
  485. * @param bool $not
  486. * @param bool $or
  487. * @return Sql
  488. * @throws \Exception
  489. */
  490. public function in($field, array $values, $not = false, $or = false)
  491. {
  492. if(!count($values)) {
  493. throw new \Exception('Sql::in() requires an array of values that are not empty!');
  494. }
  495. if($not) {
  496. $stmt = $this->_operators['notIn'];
  497. } else {
  498. $stmt = $this->_operators['in'];
  499. }
  500. $keys = $values;
  501. foreach($keys as $key => $val) {
  502. $keys[$key] = $this->_param($field, $val);
  503. }
  504. $stmt = $field.' '.$stmt.' ('.join(',', $keys).')';
  505. $values = array_combine($keys, array_values($values));
  506. return $this->where($stmt, $values, $or);
  507. }
  508. public function insert($tableName, $data, $modifiers = array(), $on_duplicate_update = array())
  509. {
  510. $this->_insert = array($tableName, $data, $modifiers, $on_duplicate_update);
  511. return $this;
  512. }
  513. /**
  514. * @param string|array $table
  515. * @param string $on
  516. * @param array $columns
  517. * @param string $type
  518. * @return Sql
  519. */
  520. public function join($table, $on, array $columns = array(), $type = 'inner')
  521. {
  522. if(is_null($type)) {
  523. $type = 'inner';
  524. }
  525. if($table instanceof $this) {
  526. $table = $table->assemble();
  527. }
  528. $this->_join[] = array($table, $on, $columns, $type);
  529. return $this;
  530. }
  531. /**
  532. * A convenience wrapper for join
  533. * @param string|array $table
  534. * @param string $on
  535. * @param array $columns
  536. * @param string $type
  537. * @return Query\Sql
  538. */
  539. public function leftJoin($table, $on, array $columns = array())
  540. {
  541. return $this->join($table, $on, $columns, 'left');
  542. }
  543. /**
  544. * A convenience wrapper for join
  545. * @param string|array $table
  546. * @param string $on
  547. * @param array $columns
  548. * @param string $type
  549. * @return Query\Sql
  550. */
  551. public function rightJoin($table, $on, array $columns = array())
  552. {
  553. return $this->join($table, $on, $columns, 'right');
  554. }
  555. /**
  556. * @param $start
  557. * @param $count
  558. * @return Sql
  559. */
  560. public function limit($start, $count)
  561. {
  562. $this->_limit = array($start, $count);
  563. return $this;
  564. }
  565. /**
  566. * @param string
  567. * @param string
  568. * @return Sql
  569. */
  570. public function orderBy($column, $direction = 'ASC')
  571. {
  572. $this->_orderBy[$column] = $direction;
  573. return $this;
  574. }
  575. /**
  576. * @param array $queries
  577. * @return Sql
  578. */
  579. public function union(array $queries)
  580. {
  581. $this->_union = $queries;
  582. return $this;
  583. }
  584. /**
  585. * @param $tableName Name of the table (resource) you wish to update
  586. * @param $data An associative array of the fields and data to update
  587. * @return Sql
  588. */
  589. public function update($tableName, $data)
  590. {
  591. $this->_update = array($tableName, $data);
  592. return $this;
  593. }
  594. /**
  595. * @param $stmt
  596. * @param array $value
  597. * @param bool $or
  598. * @return Sql
  599. */
  600. public function where($stmt, $value = array(), $or = false)
  601. {
  602. $this->_where[] = array($stmt, $value, $or);
  603. return $this;
  604. }
  605. }