PageRenderTime 27ms CodeModel.GetById 42ms RepoModel.GetById 1ms app.codeStats 0ms

/sapphire/core/model/SQLQuery.php

https://github.com/benbruscella/vpcounselling.com
PHP | 478 lines | 197 code | 61 blank | 220 comment | 44 complexity | d3b7b9e34d111578216211944e49738e MD5 | raw file
  1. <?php
  2. /**
  3. * Object representing a SQL query.
  4. * The various parts of the SQL query can be manipulated individually.
  5. *
  6. * Caution: Only supports SELECT (default) and DELETE at the moment.
  7. *
  8. * @todo Add support for INSERT and UPDATE queries
  9. *
  10. * @package sapphire
  11. * @subpackage model
  12. */
  13. class SQLQuery {
  14. /**
  15. * An array of fields to select.
  16. * @var array
  17. */
  18. public $select = array();
  19. /**
  20. * An array of join clauses. The first one is just the table name.
  21. * @var array
  22. */
  23. public $from = array();
  24. /**
  25. * An array of filters.
  26. * @var array
  27. */
  28. public $where = array();
  29. /**
  30. * An ORDER BY clause.
  31. * @var string
  32. */
  33. public $orderby;
  34. /**
  35. * An array of fields to group by.
  36. * @var array
  37. */
  38. public $groupby = array();
  39. /**
  40. * An array of having clauses.
  41. * @var array
  42. */
  43. public $having = array();
  44. /**
  45. * A limit clause.
  46. * @var string
  47. */
  48. public $limit;
  49. /**
  50. * If this is true DISTINCT will be added to the SQL.
  51. * @var boolean
  52. */
  53. public $distinct = false;
  54. /**
  55. * If this is true, this statement will delete rather than select.
  56. * @var boolean
  57. */
  58. public $delete = false;
  59. /**
  60. * The logical connective used to join WHERE clauses. Defaults to AND.
  61. * @var string
  62. */
  63. public $connective = 'AND';
  64. /**
  65. * Keep an internal register of find/replace pairs to execute when it's time to actually get the
  66. * query SQL.
  67. */
  68. private $replacementsOld = array(), $replacementsNew = array();
  69. /**
  70. * Construct a new SQLQuery.
  71. *
  72. * @param array $select An array of fields to select.
  73. * @param array $from An array of join clauses. The first one should be just the table name.
  74. * @param array $where An array of filters, to be inserted into the WHERE clause.
  75. * @param string $orderby An ORDER BY clause.
  76. * @param array $groupby An array of fields to group by.
  77. * @param array $having An array of having clauses.
  78. * @param string $limit A LIMIT clause.
  79. *
  80. * TODO: perhaps we can quote things here instead of requiring all the parameters to be quoted
  81. * by this stage.
  82. */
  83. function __construct($select = "*", $from = array(), $where = "", $orderby = "", $groupby = "", $having = "", $limit = "") {
  84. $this->select($select);
  85. // @todo
  86. $this->from = is_array($from) ? $from : array(str_replace(array('"','`'),'',$from) => $from);
  87. $this->where($where);
  88. $this->orderby($orderby);
  89. $this->groupby($groupby);
  90. $this->having($having);
  91. $this->limit($limit);
  92. }
  93. /**
  94. * Specify the list of columns to be selected by the query.
  95. *
  96. * <code>
  97. * // pass fields to select as single parameter array
  98. * $query->select(array("Col1","Col2"))->from("MyTable");
  99. *
  100. * // pass fields to select as multiple parameters
  101. * $query->select("Col1", "Col2")->from("MyTable");
  102. * </code>
  103. *
  104. * @param mixed $fields
  105. * @return SQLQuery
  106. */
  107. public function select($fields) {
  108. if (func_num_args() > 1) {
  109. $this->select = func_get_args();
  110. } else {
  111. $this->select = is_array($fields) ? $fields : array($fields);
  112. }
  113. return $this;
  114. }
  115. /**
  116. * Specify the target table to select from.
  117. *
  118. * <code>
  119. * $query->from("MyTable"); // SELECT * FROM MyTable
  120. * </code>
  121. *
  122. * @param string $table
  123. * @return SQLQuery This instance
  124. */
  125. public function from($table) {
  126. $this->from[str_replace(array('"','`'),'',$table)] = $table;
  127. return $this;
  128. }
  129. /**
  130. * Add a LEFT JOIN criteria to the FROM clause.
  131. *
  132. * @return SQLQuery This instance
  133. */
  134. public function leftJoin($table, $onPredicate) {
  135. $this->from[$table] = "LEFT JOIN \"$table\" ON $onPredicate";
  136. return $this;
  137. }
  138. /**
  139. * Add an INNER JOIN criteria to the FROM clause.
  140. *
  141. * @return SQLQuery This instance
  142. */
  143. public function innerJoin($table, $onPredicate) {
  144. $this->from[$table] = "INNER JOIN \"$table\" ON $onPredicate";
  145. return $this;
  146. }
  147. /**
  148. * Returns true if we are already joining to the given table alias
  149. */
  150. public function isJoinedTo($tableAlias) {
  151. return isset($this->from[$tableAlias]);
  152. }
  153. /**
  154. * Pass LIMIT clause either as SQL snippet or in array format.
  155. *
  156. * @param string|array $limit
  157. * @return SQLQuery This instance
  158. */
  159. public function limit($limit) {
  160. $this->limit = $limit;
  161. return $this;
  162. }
  163. /**
  164. * Pass ORDER BY clause either as SQL snippet or in array format.
  165. *
  166. * @todo Implement passing of multiple orderby pairs in nested array syntax,
  167. * e.g. array(array('sort'=>'A','dir'=>'asc'),array('sort'=>'B'))
  168. *
  169. * @param string|array $orderby
  170. * @return SQLQuery This instance
  171. */
  172. public function orderby($orderby) {
  173. // if passed as an array, assume two array values with column and direction (asc|desc)
  174. if(is_array($orderby)) {
  175. if(!array_key_exists('sort', $orderby)) user_error('SQLQuery::orderby(): Wrong format for $orderby array', E_USER_ERROR);
  176. if(isset($orderby['sort']) && !empty($orderby['sort']) && isset($orderby['dir']) && !empty($orderby['dir'])) {
  177. $combinedOrderby = "\"" . Convert::raw2sql($orderby['sort']) . "\" " . Convert::raw2sql(strtoupper($orderby['dir']));
  178. } elseif(isset($orderby['sort']) && !empty($orderby['sort'])) {
  179. $combinedOrderby = "\"" . Convert::raw2sql($orderby['sort']) . "\"";
  180. } else {
  181. $combinedOrderby = false;
  182. }
  183. } else {
  184. $combinedOrderby = $orderby;
  185. }
  186. // If sort contains a function call, let's move the sort clause into a separate selected field.
  187. // Some versions of MySQL choke if you have a group function referenced directly in the ORDER BY
  188. if($combinedOrderby && strpos($combinedOrderby,'(') !== false && strtoupper(trim($combinedOrderby)) != DB::getConn()->random()) {
  189. // Sort can be "Col1 DESC|ASC, Col2 DESC|ASC", we need to handle that
  190. $sortParts = explode(",", $combinedOrderby);
  191. // If you have select if(X,A,B),C then the array will return 'if(X','A','B)','C'.
  192. // Turn this into 'if(X,A,B)','C' by counting brackets
  193. while(list($i,$sortPart) = each($sortParts)) {
  194. while(substr_count($sortPart,'(') > substr_count($sortPart,')')) {
  195. list($i,$nextSortPart) = each($sortParts);
  196. if($i === null) break;
  197. $sortPart .= ',' . $nextSortPart;
  198. }
  199. $lumpedSortParts[] = $sortPart;
  200. }
  201. foreach($lumpedSortParts as $i => $sortPart) {
  202. $sortPart = trim($sortPart);
  203. if(substr(strtolower($sortPart),-5) == ' desc') {
  204. $this->select[] = substr($sortPart,0,-5) . " AS _SortColumn{$i}";
  205. $newSorts[] = "_SortColumn{$i} DESC";
  206. } else if(substr(strtolower($sortPart),-4) == ' asc') {
  207. $this->select[] = substr($sortPart,0,-4) . " AS _SortColumn{$i}";
  208. $newSorts[] = "_SortColumn{$i} ASC";
  209. } else {
  210. $this->select[] = "$sortPart AS _SortColumn{$i}";
  211. $newSorts[] = "_SortColumn{$i} ASC";
  212. }
  213. }
  214. $combinedOrderby = implode(", ", $newSorts);
  215. }
  216. if(!empty($combinedOrderby)) $this->orderby = $combinedOrderby;
  217. return $this;
  218. }
  219. /**
  220. * Add a GROUP BY clause.
  221. *
  222. * @param string|array $groupby
  223. * @return SQLQuery
  224. */
  225. public function groupby($groupby) {
  226. if(is_array($groupby)) {
  227. $this->groupby = array_merge($this->groupby, $groupby);
  228. } elseif(!empty($groupby)) {
  229. $this->groupby[] = $groupby;
  230. }
  231. return $this;
  232. }
  233. /**
  234. * Add a HAVING clause.
  235. *
  236. * @param string|array $having
  237. * @return SQLQuery
  238. */
  239. public function having($having) {
  240. if(is_array($having)) {
  241. $this->having = array_merge($this->having, $having);
  242. } elseif(!empty($having)) {
  243. $this->having[] = $having;
  244. }
  245. return $this;
  246. }
  247. /**
  248. * Apply a predicate filter to the where clause.
  249. *
  250. * Accepts a variable length of arguments, which represent
  251. * different ways of formatting a predicate in a where clause:
  252. *
  253. * <code>
  254. * // the entire predicate as a single string
  255. * $query->where("Column = 'Value'");
  256. *
  257. * // an exact match predicate with a key value pair
  258. * $query->where("Column", "Value");
  259. *
  260. * // a predicate with user defined operator
  261. * $query->where("Column", "!=", "Value");
  262. * </code>
  263. *
  264. */
  265. public function where() {
  266. $args = func_get_args();
  267. if (func_num_args() == 3) {
  268. $filter = "{$args[0]} {$args[1]} '{$args[2]}'";
  269. } elseif (func_num_args() == 2) {
  270. $filter = "{$args[0]} = '{$args[1]}'";
  271. } else {
  272. $filter = $args[0];
  273. }
  274. if(is_array($filter)) {
  275. $this->where = array_merge($this->where,$filter);
  276. } elseif(!empty($filter)) {
  277. $this->where[] = $filter;
  278. }
  279. return $this;
  280. }
  281. /**
  282. * Use the disjunctive operator 'OR' to join filter expressions in the WHERE clause.
  283. */
  284. public function useDisjunction() {
  285. $this->connective = 'OR';
  286. }
  287. /**
  288. * Use the conjunctive operator 'AND' to join filter expressions in the WHERE clause.
  289. */
  290. public function useConjunction() {
  291. $this->connective = 'AND';
  292. }
  293. /**
  294. * Swap the use of one table with another.
  295. * @param string $old Name of the old table.
  296. * @param string $new Name of the new table.
  297. */
  298. function renameTable($old, $new) {
  299. $this->replaceText("`$old`", "`$new`");
  300. $this->replaceText("\"$old\"", "\"$new\"");
  301. }
  302. /**
  303. * Swap some text in the SQL query with another.
  304. * @param string $old The old text.
  305. * @param string $new The new text.
  306. */
  307. function replaceText($old, $new) {
  308. $this->replacementsOld[] = $old;
  309. $this->replacementsNew[] = $new;
  310. }
  311. /**
  312. * Return an SQL WHERE clause to filter a SELECT query.
  313. *
  314. * @return string
  315. */
  316. function getFilter() {
  317. return ($this->where) ? implode(") {$this->connective} (" , $this->where) : '';
  318. }
  319. /**
  320. * Generate the SQL statement for this query.
  321. *
  322. * @return string
  323. */
  324. function sql() {
  325. $sql = DB::getConn()->sqlQueryToString($this);
  326. if($this->replacementsOld) $sql = str_replace($this->replacementsOld, $this->replacementsNew, $sql);
  327. return $sql;
  328. }
  329. /**
  330. * Return the generated SQL string for this query
  331. *
  332. * @return string
  333. */
  334. function __toString() {
  335. return $this->sql();
  336. }
  337. /**
  338. * Execute this query.
  339. * @return SS_Query
  340. */
  341. function execute() {
  342. return DB::query($this->sql(), E_USER_ERROR);
  343. }
  344. /**
  345. * Checks whether this query is for a specific ID in a table
  346. *
  347. * @todo Doesn't work with combined statements (e.g. "Foo='bar' AND ID=5")
  348. *
  349. * @return boolean
  350. */
  351. function filtersOnID() {
  352. $regexp = '/^(.*\.)?("|`)?ID("|`)?\s?=/';
  353. // Sometimes the ID filter will be the 2nd element, if there's a ClasssName filter first.
  354. if(isset($this->where[0]) && preg_match($regexp, $this->where[0])) return true;
  355. if(isset($this->where[1]) && preg_match($regexp, $this->where[1])) return true;
  356. return false;
  357. }
  358. /**
  359. * Checks whether this query is filtering on a foreign key, ie finding a has_many relationship
  360. *
  361. * @todo Doesn't work with combined statements (e.g. "Foo='bar' AND ParentID=5")
  362. *
  363. * @return boolean
  364. */
  365. function filtersOnFK() {
  366. return (
  367. $this->where
  368. && preg_match('/^(.*\.)?("|`)?[a-zA-Z]+ID("|`)?\s?=/', $this->where[0])
  369. );
  370. }
  371. /// VARIOUS TRANSFORMATIONS BELOW
  372. /**
  373. * Return the number of rows in this query if the limit were removed. Useful in paged data sets.
  374. * @return int
  375. */
  376. function unlimitedRowCount( $column = null) {
  377. // we can't clear the select if we're relying on its output by a HAVING clause
  378. if(count($this->having)) {
  379. $records = $this->execute();
  380. return $records->numRecords();
  381. }
  382. $clone = clone $this;
  383. $clone->limit = null;
  384. $clone->orderby = null;
  385. // Choose a default column
  386. if($column == null) {
  387. if($this->groupby) {
  388. $countQuery = new SQLQuery();
  389. $countQuery->select = array("count(*)");
  390. $countQuery->from = array('(' . $clone->sql() . ') as all_distinct');
  391. return $countQuery->execute()->value();
  392. } else {
  393. $clone->select = array("count(*)");
  394. }
  395. } else {
  396. $clone->select = array("count($column)");
  397. }
  398. $clone->groupby = null;
  399. return $clone->execute()->value();
  400. }
  401. /**
  402. * Returns true if this query can be sorted by the given field.
  403. * Note that the implementation of this method is a little crude at the moment, it wil return
  404. * "false" more often that is strictly necessary.
  405. */
  406. function canSortBy($fieldName) {
  407. $fieldName = preg_replace('/(\s+?)(A|DE)SC$/', '', $fieldName);
  408. $sql = $this->sql();
  409. $selects = $this->select;
  410. foreach($selects as $i => $sel) {
  411. if (preg_match('/"(.*)"\."(.*)"/', $sel, $matches)) $selects[$i] = $matches[2];
  412. }
  413. $SQL_fieldName = Convert::raw2sql($fieldName);
  414. return (in_array($SQL_fieldName,$selects) || stripos($sql,"AS {$SQL_fieldName}"));
  415. }
  416. }
  417. ?>