KXStudio Website https://kx.studio/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

450 lines
17KB

  1. <?php
  2. /**
  3. * Class: QueryBuilder
  4. * A generic SQL query builder.
  5. */
  6. class QueryBuilder {
  7. /**
  8. * Function: build_select
  9. * Creates a full SELECT query.
  10. *
  11. * Parameters:
  12. * $tables - Tables to select from.
  13. * $fields - Columns to select.
  14. * $order - What to order by.
  15. * $limit - Limit of the result.
  16. * $offset - Starting point for the result.
  17. * $group - What to group by.
  18. * $left_join - Any @LEFT JOIN@s to add.
  19. * &$params - An associative array of parameters used in the query.
  20. *
  21. * Returns:
  22. * A @SELECT@ query string.
  23. */
  24. public static function build_select($tables,
  25. $fields,
  26. $conds,
  27. $order = null,
  28. $limit = null,
  29. $offset = null,
  30. $group = null,
  31. $left_join = array(),
  32. &$params = array()) {
  33. $query = "SELECT ".self::build_select_header($fields, $tables)."\n".
  34. "FROM ".self::build_from($tables)."\n";
  35. foreach ($left_join as $join)
  36. $query.= "LEFT JOIN __".$join["table"]." ON ".self::build_where($join["where"], $join["table"], $params)."\n";
  37. $query.= ($conds ? "WHERE ".self::build_where($conds, $tables, $params)."\n" : "").
  38. ($group ? "GROUP BY ".self::build_group($group, $tables)."\n" : "").
  39. ($order ? "ORDER BY ".self::build_order($order, $tables)."\n" : "").
  40. self::build_limits($offset, $limit);
  41. return $query;
  42. }
  43. /**
  44. * Function: build_insert
  45. * Creates a full insert query.
  46. *
  47. * Parameters:
  48. * $table - Table to insert into.
  49. * $data - Data to insert.
  50. * &$params - An associative array of parameters used in the query.
  51. *
  52. * Returns:
  53. * An @INSERT@ query string.
  54. */
  55. public static function build_insert($table, $data, &$params = array()) {
  56. if (empty($params))
  57. foreach ($data as $key => $val)
  58. $params[":".str_replace(array("(", ")", "."), "_", $key)] = $val;
  59. return "INSERT INTO __$table\n".
  60. self::build_insert_header($data)."\n".
  61. "VALUES\n".
  62. "(".implode(", ", array_keys($params)).")\n";
  63. }
  64. /**
  65. * Function: build_update
  66. * Creates a full update query.
  67. *
  68. * Parameters:
  69. * $table - Table to update.
  70. * $conds - Conditions to update rows by.
  71. * $data - Data to update.
  72. * &$params - An associative array of parameters used in the query.
  73. *
  74. * Returns:
  75. * An @UPDATE@ query string.
  76. */
  77. public static function build_update($table, $conds, $data, &$params = array()) {
  78. return "UPDATE __$table\n".
  79. "SET ".self::build_update_values($data, $params)."\n".
  80. ($conds ? "WHERE ".self::build_where($conds, $table, $params) : "");
  81. }
  82. /**
  83. * Function: build_delete
  84. * Creates a full delete query.
  85. *
  86. * Parameters:
  87. * $table - Table to delete from.
  88. * $conds - Conditions to delete by.
  89. * &$params - An associative array of parameters used in the query.
  90. *
  91. * Returns:
  92. * A @DELETE@ query string.
  93. */
  94. public static function build_delete($table, $conds, &$params = array()) {
  95. return "DELETE FROM __$table\n".
  96. ($conds ? "WHERE ".self::build_where($conds, $table, $params) : "");
  97. }
  98. /**
  99. * Function: build_update_values
  100. * Creates an update data part.
  101. *
  102. * Parameters:
  103. * $data - Data to update.
  104. * &$params - An associative array of parameters used in the query.
  105. */
  106. public static function build_update_values($data, &$params = array()) {
  107. $set = self::build_conditions($data, $params, null, true);
  108. return implode(",\n ", $set);
  109. }
  110. /**
  111. * Function: build_insert_header
  112. * Creates an insert header.
  113. *
  114. * Parameters:
  115. * $data - Data to insert.
  116. */
  117. public static function build_insert_header($data) {
  118. $set = array();
  119. foreach (array_keys($data) as $field)
  120. array_push($set, self::safecol($field));
  121. return "(".implode(", ", $set).")";
  122. }
  123. /**
  124. * Function: build_limits
  125. * Creates the LIMIT part of a query.
  126. *
  127. * Parameters:
  128. * $offset - Offset of the result.
  129. * $limit - Limit of the result.
  130. */
  131. public static function build_limits($offset, $limit) {
  132. if ($limit === null)
  133. return "";
  134. if ($offset !== null)
  135. return "LIMIT ".$offset.", ".$limit;
  136. return "LIMIT ".$limit;
  137. }
  138. /**
  139. * Function: build_from
  140. * Creates a FROM header for select queries.
  141. *
  142. * Parameters:
  143. * $tables - Tables to select from.
  144. */
  145. public static function build_from($tables) {
  146. if (!is_array($tables))
  147. $tables = array($tables);
  148. foreach ($tables as &$table)
  149. if (substr($table, 0, 2) != "__")
  150. $table = "__".$table;
  151. return implode(",\n ", $tables);
  152. }
  153. /**
  154. * Function: build_count
  155. * Creates a SELECT COUNT(1) query.
  156. *
  157. * Parameters:
  158. * $tables - Tables to tablefy with.
  159. * $conds - Conditions to select by.
  160. * &$params - An associative array of parameters used in the query.
  161. */
  162. public static function build_count($tables, $conds, &$params = array()) {
  163. return "SELECT COUNT(1) AS count\n".
  164. "FROM ".self::build_from($tables)."\n".
  165. ($conds ? "WHERE ".self::build_where($conds, $tables, $params) : "");
  166. }
  167. /**
  168. * Function: build_select_header
  169. * Creates a SELECT fields header.
  170. *
  171. * Parameters:
  172. * $fields - Columns to select.
  173. * $tables - Tables to tablefy with.
  174. */
  175. public static function build_select_header($fields, $tables = null) {
  176. if (!is_array($fields))
  177. $fields = array($fields);
  178. $tables = (array) $tables;
  179. foreach ($fields as &$field) {
  180. self::tablefy($field, $tables);
  181. $field = self::safecol($field);
  182. }
  183. return implode(",\n ", $fields);
  184. }
  185. /**
  186. * Function: build_where
  187. * Creates a WHERE query.
  188. */
  189. public static function build_where($conds, $tables = null, &$params = array()) {
  190. $conds = (array) $conds;
  191. $tables = (array) $tables;
  192. $conditions = self::build_conditions($conds, $params, $tables);
  193. return (empty($conditions)) ? "" : "(".implode(")\n AND (", array_filter($conditions)).")";
  194. }
  195. /**
  196. * Function: build_group
  197. * Creates a GROUP BY argument.
  198. *
  199. * Parameters:
  200. * $order - Columns to group by.
  201. * $tables - Tables to tablefy with.
  202. */
  203. public static function build_group($by, $tables = null) {
  204. $by = (array) $by;
  205. $tables = (array) $tables;
  206. foreach ($by as &$column) {
  207. self::tablefy($column, $tables);
  208. $column = self::safecol($column);
  209. }
  210. return implode(",\n ", array_unique(array_filter($by)));
  211. }
  212. /**
  213. * Function: build_order
  214. * Creates an ORDER BY argument.
  215. *
  216. * Parameters:
  217. * $order - Columns to order by.
  218. * $tables - Tables to tablefy with.
  219. */
  220. public static function build_order($order, $tables = null) {
  221. $tables = (array) $tables;
  222. if (!is_array($order))
  223. $order = comma_sep($order);
  224. foreach ($order as &$by) {
  225. self::tablefy($by, $tables);
  226. $by = self::safecol($by);
  227. }
  228. return implode(",\n ", $order);
  229. }
  230. /**
  231. * Function: build_list
  232. * Returns ('one', 'two', '', 1, 0) from array("one", "two", null, true, false)
  233. */
  234. public static function build_list($vals, $params = array()) {
  235. $return = array();
  236. foreach ($vals as $val) {
  237. if (is_object($val)) # Useful catch, e.g. empty SimpleXML objects.
  238. $val = "";
  239. $return[] = (isset($params[$val])) ? $val : SQL::current()->escape($val) ;
  240. }
  241. return "(".join(", ", $return).")";
  242. }
  243. /**
  244. * Function: safecol
  245. * Wraps a column in proper escaping if it is a SQL keyword.
  246. *
  247. * Doesn't check every keyword, just the common/sensible ones.
  248. *
  249. * ...Okay, it only does two. "order" and "group".
  250. *
  251. * Parameters:
  252. * $name - Name of the column.
  253. */
  254. public static function safecol($name) {
  255. return preg_replace("/(([^a-zA-Z0-9_]|^)(order|group)([^a-zA-Z0-9_]|
  256. $))/i",
  257. (SQL::current()->adapter == "mysql") ? "\\2`\\3`
  258. \\4" : '\\2"\\3"\\4',
  259. $name);
  260. }
  261. /**
  262. * Function: build_conditions
  263. * Builds an associative array of SQL values into PDO-esque paramized query strings.
  264. *
  265. * Parameters:
  266. * $conds - Conditions.
  267. * &$params - Parameters array to fill.
  268. * $tables - If specified, conditions will be tablefied with these tables.
  269. * $insert - Is this an insert/update query?
  270. */
  271. public static function build_conditions($conds, &$params, $tables = null, $insert = false) {
  272. $conditions = array();
  273. foreach ($conds as $key => $val) {
  274. if (is_int($key)) # Full expression
  275. $cond = $val;
  276. else { # Key => Val expression
  277. if (is_string($val) and strlen($val) and $val[0] == ":")
  278. $cond = self::safecol($key)." = ".$val;
  279. else {
  280. if (is_bool($val))
  281. $val = (int) $val;
  282. if (substr($key, -4) == " not") { # Negation
  283. $key = self::safecol(substr($key, 0, -4));
  284. $param = str_replace(array("(", ")", "."), "_", $key);
  285. if (is_array($val))
  286. $cond = $key." NOT IN ".self::build_list($val, $params);
  287. elseif ($val === null)
  288. $cond = $key." IS NOT NULL";
  289. else {
  290. $cond = $key." != :".$param;
  291. $params[":".$param] = $val;
  292. }
  293. } elseif (substr($key, -5) == " like" and is_array($val)) { # multiple LIKE
  294. $key = self::safecol(substr($key, 0, -5));
  295. $likes = array();
  296. foreach ($val as $index => $match) {
  297. $param = str_replace(array("(", ")", "."), "_", $key)."_".$index;
  298. $likes[] = $key." LIKE :".$param;
  299. $params[":".$param] = $match;
  300. }
  301. $cond = "(".implode(" OR ", $likes).")";
  302. } elseif (substr($key, -9) == " like all" and is_array($val)) { # multiple LIKE
  303. $key = self::safecol(substr($key, 0, -9));
  304. $likes = array();
  305. foreach ($val as $index => $match) {
  306. $param = str_replace(array("(", ")", "."), "_", $key)."_".$index;
  307. $likes[] = $key." LIKE :".$param;
  308. $params[":".$param] = $match;
  309. }
  310. $cond = "(".implode(" AND ", $likes).")";
  311. } elseif (substr($key, -9) == " not like" and is_array($val)) { # multiple NOT LIKE
  312. $key = self::safecol(substr($key, 0, -9));
  313. $likes = array();
  314. foreach ($val as $index => $match) {
  315. $param = str_replace(array("(", ")", "."), "_", $key)."_".$index;
  316. $likes[] = $key." NOT LIKE :".$param;
  317. $params[":".$param] = $match;
  318. }
  319. $cond = "(".implode(" AND ", $likes).")";
  320. } elseif (substr($key, -5) == " like") { # LIKE
  321. $key = self::safecol(substr($key, 0, -5));
  322. $param = str_replace(array("(", ")", "."), "_", $key);
  323. $cond = $key." LIKE :".$param;
  324. $params[":".$param] = $val;
  325. } elseif (substr($key, -9) == " not like") { # NOT LIKE
  326. $key = self::safecol(substr($key, 0, -9));
  327. $param = str_replace(array("(", ")", "."), "_", $key);
  328. $cond = $key." NOT LIKE :".$param;
  329. $params[":".$param] = $val;
  330. } elseif (substr_count($key, " ")) { # Custom operation, e.g. array("foo >" => $bar)
  331. list($param,) = explode(" ", $key);
  332. $param = str_replace(array("(", ")", "."), "_", $param);
  333. $cond = self::safecol($key)." :".$param;
  334. $params[":".$param] = $val;
  335. } else { # Equation
  336. if (is_array($val))
  337. $cond = self::safecol($key)." IN ".self::build_list($val, $params);
  338. elseif ($val === null and $insert)
  339. $cond = self::safecol($key)." = ''";
  340. elseif ($val === null)
  341. $cond = self::safecol($key)." IS NULL";
  342. else {
  343. $param = str_replace(array("(", ")", "."), "_", $key);
  344. $cond = self::safecol($key)." = :".$param;
  345. $params[":".$param] = $val;
  346. }
  347. }
  348. }
  349. }
  350. if ($tables)
  351. self::tablefy($cond, $tables);
  352. $conditions[] = $cond;
  353. }
  354. return $conditions;
  355. }
  356. /**
  357. * Function: tablefy
  358. * Automatically prepends tables and table prefixes to a field if it doesn't already have them.
  359. *
  360. * Parameters:
  361. * &$field - The field to "tablefy".
  362. * $tables - An array of tables. The first one will be used for prepending.
  363. */
  364. public static function tablefy(&$field, $tables) {
  365. if (!preg_match_all("/(\(|[\s]+|^)(?!__)([a-z0-9_\.\*]+)(\)|[\s]+|$)/", $field, $matches))
  366. return $field = str_replace("`", "", $field); # Method for bypassing the prefixer.
  367. foreach ($matches[0] as $index => $full) {
  368. $before = $matches[1][$index];
  369. $name = $matches[2][$index];
  370. $after = $matches[3][$index];
  371. if (is_numeric($name))
  372. continue;
  373. # Does it not already have a table specified?
  374. if (!substr_count($full, ".")) {
  375. # Don't replace things that are already either prefixed or paramized.
  376. $field = preg_replace("/([^\.:'\"_]|^)".preg_quote($full, "/")."/",
  377. "\\1".$before."__".$tables[0].".".$name.$after,
  378. $field,
  379. 1);
  380. } else {
  381. # Okay, it does, but is the table prefixed?
  382. if (substr($full, 0, 2) != "__") {
  383. # Don't replace things that are already either prefixed or paramized.
  384. $field = preg_replace("/([^\.:'\"_]|^)".preg_quote($full, "/")."/",
  385. "\\1".$before."__".$name.$after,
  386. $field,
  387. 1);
  388. }
  389. }
  390. }
  391. $field = preg_replace("/AS ([^ ]+)\./i", "AS ", $field);
  392. }
  393. }