Query.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. <?php
  2. namespace PHPixie\DB;
  3. /**
  4. * Query builder. It allows building queries by using methods to set specific query parameters.
  5. * Database drivers extend this class so that they can generate database specific queries.
  6. * The idea is to provide a database agnostic interface to query writing.
  7. *
  8. * @method mixed table(string $table = null) Set table to query.
  9. * Without arguments returns current table, returns self otherwise.
  10. *
  11. * @method mixed data(array $data = null) Set data for insert or update queries.
  12. * Without arguments returns current data, returns self otherwise.
  13. *
  14. * @method mixed limit(int $limit = null) Set number of rows to return. If NULL is passed than no limit is used.
  15. * If NULL is passed than no limit is used.
  16. * Without arguments returns current limit, returns self otherwise.
  17. *
  18. * @method mixed offset(string $offset = null) Set the offset for the first row in result.
  19. * If NULL is passed than no offset is used.
  20. * Without arguments returns current offset, returns self otherwise.
  21. *
  22. * @method mixed group_by(string $group_by = null) A column to group rows by for aggregator functions.
  23. * If NULL is passed than no grouping is done.
  24. * Without arguments returns current group_by argument, returns self otherwise.
  25. *
  26. * @method mixed type(string $type = null) Set query type. Available types: select, update, insert, delete, count.
  27. * Without arguments returns current type argument, returns self otherwise.
  28. * @package Database
  29. */
  30. abstract class Query
  31. {
  32. /**
  33. * Array of conditions that rows must meet
  34. * @var array
  35. */
  36. protected $_conditions = array();
  37. /**
  38. * Table to query
  39. * @var unknown
  40. */
  41. protected $_table;
  42. /**
  43. * Fields to return in the query
  44. * @var array
  45. */
  46. protected $_fields;
  47. /**
  48. * Data for row insertion or update
  49. * @var unknown
  50. */
  51. protected $_data;
  52. /**
  53. * Query type. Available types: select, update, insert, delete, count
  54. * @var string
  55. */
  56. protected $_type;
  57. /**
  58. * Parameters for tables to join
  59. * @var array
  60. */
  61. protected $_joins = array();
  62. /**
  63. * Number of rows to return
  64. * @var int
  65. */
  66. protected $_limit;
  67. /**
  68. * Offset of the first row
  69. * @var int
  70. */
  71. protected $_offset;
  72. /**
  73. * Columns and directions to order by
  74. * @var array
  75. */
  76. protected $_order_by = array();
  77. /**
  78. * Database connection
  79. * @var DB
  80. */
  81. protected $_db;
  82. /**
  83. * Conditions for aggregator functions
  84. * @var array
  85. */
  86. protected $_having = array();
  87. /**
  88. * Column to group by for aggregator functions
  89. * @var string
  90. */
  91. protected $_group_by;
  92. /**
  93. * Last alias used on the table
  94. * @var string
  95. */
  96. protected $_alias = null;
  97. /**
  98. * Methods and type of value they allow that are available via __call
  99. * @var array
  100. */
  101. protected $methods = array('data' => 'array', 'limit' => array('integer', 'NULL'), 'offset' => array('integer', 'NULL'), 'group_by' => array('string', 'NULL'), 'type' => 'string');
  102. /**
  103. * UNION queries
  104. * @var array
  105. */
  106. protected $_union = array();
  107. /**
  108. * Generates a query in format that can be executed on current database implementation
  109. *
  110. */
  111. public abstract function query();
  112. /**
  113. * Creates a new query
  114. *
  115. * @param DB $db Database connection
  116. * @param string $type Query type. Available types: select, update, insert, delete, count
  117. * @return void
  118. */
  119. public function __construct($db, $type)
  120. {
  121. $this->_db = $db;
  122. $this->_type = $type;
  123. }
  124. /**
  125. * Sets fields to be queried from the database. You can add aliases to the fields
  126. * by passing them as:
  127. *
  128. * array('field_name','alias')
  129. *
  130. * Example: $query->fields('id', array('name', 'fairy_name'))
  131. *
  132. * @param mixed $field,... Fields to be selected from the table
  133. * @return mixed If no parameters are passed returns current array of fields,
  134. * otherwise returns self.
  135. */
  136. public function fields()
  137. {
  138. $p = func_get_args();
  139. if (empty($p))
  140. {
  141. return $this->_fields;
  142. }
  143. else
  144. {
  145. $this->_fields = $p;
  146. }
  147. return $this;
  148. }
  149. /**
  150. * Sets the table to perform operations on, also supports subqueries
  151. *
  152. * @param string|Query_database|Expression_database $table table to select from
  153. * @param string $alias Alias for this table
  154. * @return mixed Returns self if a table is passed, otherwise returns the table
  155. */
  156. public function table($table = null, $alias = null)
  157. {
  158. if ($table == null)
  159. {
  160. return is_array($this->_table) ? $this->_table[1] : $this->_table;
  161. }
  162. if (!is_string($table) && $alias == null)
  163. {
  164. $alias = $this->add_alias();
  165. }
  166. $this->_table = $alias == null ? $table : array($table, $alias);
  167. return $this;
  168. }
  169. /**
  170. * Magic methods to create methods for all generic query parts
  171. *
  172. * @param string $method Name of the method to call
  173. * @param array $args Array of parameters
  174. * @return mixed If no arguments are passed returns the current value of the property,
  175. * otherwise returns self.
  176. * @throws \Exception If method doesn't exist
  177. * @throws \Exception If value is of incorrect type
  178. * @see $methods
  179. */
  180. public function __call($method, $args)
  181. {
  182. if (isset($this->methods[$method]))
  183. {
  184. $property = '_'.$method;
  185. if (empty($args))
  186. {
  187. return $this->$property;
  188. }
  189. $val = $args[0];
  190. if (is_numeric($val))
  191. {
  192. $val = (int) $val;
  193. }
  194. $allowed_types = $this->methods[$method];
  195. if (!is_array($allowed_types))
  196. {
  197. $allowed_types = array($allowed_types);
  198. }
  199. if (!in_array(gettype($val), $allowed_types))
  200. {
  201. throw new \Exception("Method '{$method}' only accepts values of type: ".implode(' or ', $allowed_types).", '{$val}' was passed");
  202. }
  203. $this->$property = $val;
  204. return $this;
  205. }
  206. throw new \Exception("Method '{$method}' doesn't exist.");
  207. }
  208. /**
  209. * Executes the query
  210. *
  211. * @return object Executes current query on its database connection
  212. * @see DB
  213. */
  214. public function execute()
  215. {
  216. $query = $this->query();
  217. $result = $this->_db->execute($query[0], $query[1]);
  218. if ($this->_type == 'count')
  219. {
  220. return $result->get('count');
  221. }
  222. return $result;
  223. }
  224. /**
  225. * Adds a joined table to the query.
  226. *
  227. * @param string $table Table to join
  228. * @param array $conds Conditions to join tables on, same behavior as with where() method
  229. * @param string $type Type of join. Defaults to 'left'
  230. * @return Query_Database Returns self
  231. * @see where()
  232. */
  233. public function join($table, $conds, $type = 'left')
  234. {
  235. $this->_joins[] = array($table, $type, $this->get_condition_part($conds));
  236. return $this;
  237. }
  238. /**
  239. * Sets conditions for aggregate functions, same behavior as with where() method
  240. *
  241. * @return Query_Database Returns self
  242. * @see where()
  243. */
  244. public function having()
  245. {
  246. $p = func_get_args();
  247. $cond = $this->get_condition_part($p);
  248. $this->_having = array_merge($this->_having, array($cond));
  249. return $this;
  250. }
  251. /**
  252. * Adds a column to ordering parameters.
  253. *
  254. * @param string $column Column to order by
  255. * @param string $dir Ordering direction.
  256. * @return Query_Database Returns self
  257. * @throws \Exception If ordering direction isn't DESC or ASC
  258. */
  259. public function order_by($column, $dir = 'ASC')
  260. {
  261. $dir = strtoupper($dir);
  262. if ($dir != 'DESC' && $dir != 'ASC')
  263. {
  264. throw new \Exception("Invalid sorting direction {$dir} specified");
  265. }
  266. $this->_order_by[] = array($column, $dir);
  267. return $this;
  268. }
  269. /**
  270. * Sets conditions for the query.
  271. * Can be called in many ways, examples:
  272. * Shorthand equals condition:
  273. * <code>
  274. * $q->where('name', 'Tinkerbell')
  275. * </code>
  276. * Conditions with operator:
  277. * <code>
  278. * $q->where('id', '>', 3)
  279. * </code>
  280. * OR logic:
  281. * <code>
  282. * $q->where('or', array('name', 'Tinkerbell'))
  283. * </code>
  284. * OR logic with operator
  285. * <code>
  286. * ->where('or', array('id', '>', 3))
  287. * </code>
  288. * Arrays represent brackets. e.g
  289. * <code>
  290. * $q->where('name', 'Tinkerbell')
  291. * ->where('or', array(
  292. * array('id', '>', 7),
  293. * array('id', '<', 15)
  294. * );
  295. * //Will produce "WHERE `name`='Tinkerbell' OR (`id` > 7 AND `id` < 15)"
  296. * </code>
  297. * Multiple calls to where() append new conditions to previous ones
  298. *
  299. * @param mixed $column Column name, logic parameter 'OR' or 'AND' or an array of conditions
  300. * @param mixed $operator Condition value, operator or an array of parameters
  301. * @param mixed $val Condition value
  302. *
  303. * @return Query_Database Returns self
  304. */
  305. public function where()
  306. {
  307. $p = func_get_args();
  308. $cond = $this->get_condition_part($p);
  309. $this->_conditions = array_merge($this->_conditions, array($cond));
  310. return $this;
  311. }
  312. /**
  313. * Recursively builds condition arrays for methods like where(), having()
  314. *
  315. * @param array $p Parameters passed to the method
  316. * @return array Array in condition format
  317. * @throws \Exception If condition format is incorrect
  318. */
  319. private function get_condition_part($p)
  320. {
  321. if (is_string($p[0]) && (strtolower($p[0]) == 'or' || strtolower($p[0]) == 'and') && isset($p[1]) && is_array($p[1]))
  322. {
  323. $cond = $this->get_condition_part($p[1]);
  324. $cond['logic'] = strtolower($p[0]);
  325. return $cond;
  326. }
  327. if (is_array($p[0]))
  328. {
  329. if (count($p) == 1)
  330. {
  331. return $this->get_condition_part($p[0]);
  332. }
  333. $conds = array();
  334. foreach ($p as $q)
  335. {
  336. $conds[] = $this->get_condition_part($q);
  337. }
  338. if (count($conds) == 1)
  339. {
  340. return $conds;
  341. }
  342. return array('logic' => 'and', 'conditions' => $conds);
  343. }
  344. if ((is_string($p[0]) || $p[0] instanceof \PHPixie\DB\Expression) && isset($p[1]))
  345. {
  346. if (is_string($p[0]) && strpos($p[0], '.') === false)
  347. {
  348. $p[0] = $this->last_alias().'.'.$p[0];
  349. }
  350. return array(
  351. 'logic' => 'and',
  352. 'conditions' => array(
  353. 'field' => $p[0],
  354. 'operator' => isset($p[2]) ? $p[1] : '=',
  355. 'value' => isset($p[2]) ? $p[2] : $p[1]
  356. )
  357. );
  358. }
  359. throw new \Exception('Incorrect conditional statement passed');
  360. }
  361. /**
  362. * Adds a UNION to the query
  363. *
  364. * @param Query_Database|Expression_Database $query Query for the UNION
  365. * @param string $all whether to do a UNION ALL, e.g. keep duplicate rows
  366. * @return Query_Database Returns self
  367. */
  368. public function union($query,$all=true) {
  369. $this->_union[] = array($query,$all);
  370. return $this;
  371. }
  372. /**
  373. * Gets last generated alias
  374. *
  375. * @return string Last generated alias. If no alias were created returns table name.
  376. */
  377. public function last_alias()
  378. {
  379. if ($this->_alias === null)
  380. {
  381. return $this->_table;
  382. }
  383. return 'a'.$this->_alias;
  384. }
  385. /**
  386. * Generates new alias. Useful for dynamically adding aliases to joins.
  387. * Alias is just a letter 'a' with an incremented number.
  388. *
  389. * @return string New alias
  390. */
  391. public function add_alias()
  392. {
  393. if ($this->_alias === null)
  394. {
  395. $this->_alias = 0;
  396. }
  397. else
  398. {
  399. $this->_alias++;
  400. }
  401. return $this->last_alias();
  402. }
  403. }