PageRenderTime 28ms CodeModel.GetById 1ms RepoModel.GetById 0ms app.codeStats 1ms

/app/class/queryBuilder.php

http://pos-tracker.googlecode.com/
PHP | 409 lines | 235 code | 1 blank | 173 comment | 32 complexity | 7c14f7fc3558da69a544c29d778b0802 MD5 | raw file
Possible License(s): LGPL-2.1
  1. <?php
  2. /**
  3. * Pos-Tracker
  4. *
  5. * queryBuilder.php - Builds SQL queries. Modified from yapeal.
  6. *
  7. * PHP version 5
  8. *
  9. * LICENSE: This file is part of POS-Tracker.
  10. * POS-Tracker is free software: you can redistribute it and/or modify
  11. * it under the terms of the GNU General Public License as published by
  12. * the Free Software Foundation, version 3 of the License.
  13. *
  14. * POS-Tracker is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU General Public License
  20. * along with POS-Tracker. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. * @author Stephen Gulickk <stephenmg12@gmail.com>
  23. * @author Michael Cummings <mgcummings@yahoo.com>
  24. * @copyright 2007-2011 (C) Stephen Gulick and Michael Cummings
  25. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL 3.0
  26. * @package POS-Tracker
  27. * @version SVN: $Id$
  28. * @link http://code.google.com/p/pos-tracker/
  29. * @link http://code.google.com/p/yapeal/
  30. * @link http://www.eveonline.com/
  31. */
  32. /**
  33. * @internal Allow viewing of the source code in web browser.
  34. */
  35. if (isset($_REQUEST['viewSource'])) {
  36. highlight_file(__FILE__);
  37. exit();
  38. };
  39. /**
  40. * @internal Only let this code be included or required not ran directly.
  41. */
  42. if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) {
  43. exit();
  44. };
  45. /**
  46. * Class used to build SQL queries.
  47. *
  48. * @package Yapeal
  49. * @subpackage YapealQueryBuilder
  50. */
  51. class queryBuilder implements Countable {
  52. /**
  53. * @var mixed Holds count for auto store.
  54. */
  55. protected $autoStore;
  56. /**
  57. * @var string List of column ADOFieldObjects for table.
  58. */
  59. protected $colObjects = array();
  60. /**
  61. * @var array List of columns and their generic ADO types.
  62. */
  63. protected $colTypes = array();
  64. /**
  65. * @var string Holds an instance of the DB connection.
  66. */
  67. protected $con;
  68. /**
  69. * @var array Holds a list of default column values.
  70. */
  71. protected $defaults = array();
  72. /**
  73. * @var array Holds the built rows of data to be inserted.
  74. */
  75. protected $rows = array();
  76. /**
  77. * @var integer Holds current number of rows.
  78. */
  79. private $rowCount = 0;
  80. /**
  81. * @var string Holds the table name of the query is being built.
  82. */
  83. protected $tableName;
  84. /**
  85. * Constructor
  86. *
  87. * @param string $tableName Name of the table this query is for.
  88. * @param string $dsn ADOdb DSN for database connection.
  89. * @param mixed $autoStore Sets how many rows can be added before they are
  90. * automatically stored. Set to FALSE to turn off.
  91. *
  92. * @throws InvalidArgumentException Throws InvalidArgumentException if
  93. * $tableName or $dsn aren't strings.
  94. * @throws RuntimeException Throws RuntimeException if can't get ADOdb
  95. * connection or table column information.
  96. */
  97. public function __construct($tableName, $dsn, $autoStore =10) {
  98. if (!is_string($tableName)) {
  99. $mess = '$tableName must be a string in ' . __CLASS__;
  100. trigger_error($mess, E_USER_ERROR);
  101. };// if !is_string $params[$k] ...
  102. // Keep table name for later.
  103. $this->tableName = $tableName;
  104. if (!is_string($dsn)) {
  105. $mess = '$dsn must be a string in ' . __CLASS__;
  106. trigger_error($mess, E_USER_ERROR);
  107. };// if !is_string $params[$k] ...
  108. //$this->dsn = $dsn;
  109. try {
  110. // Get a database connection.
  111. $this->con = DBConnection::connect($dsn);
  112. }
  113. catch (ADODB_Exception $e) {
  114. $mess = 'Failed to get database connection in ' . __CLASS__;
  115. trigger_error($mess, E_USER_ERROR);
  116. }
  117. try {
  118. // Get a list of column objects.
  119. $this->colObjects = $this->con->MetaColumns($tableName, FALSE);
  120. }
  121. catch (ADODB_Exception $e) {
  122. $mess = 'Failed to get ADOFieldObjects for columns in ' . __CLASS__;
  123. trigger_error($mess, E_USER_ERROR);
  124. }
  125. // Extract some column information into more useful forms.
  126. foreach ($this->colObjects as $col) {
  127. // Add any columns with default values to default list.
  128. if (isset($col->has_default) && $col->has_default === TRUE) {
  129. $this->defaults[$col->name] = $col->default_value;
  130. };// if isset $col->has_default ...
  131. // Make list of column names and their ADOdb generic types.
  132. $this->colTypes[$col->name] = $this->metaType($col);
  133. };// foreach $this->columns ...
  134. $this->autoStore = $autoStore;
  135. }// function __construct
  136. /**
  137. * Destructor used to make sure to release ADOdb resource correctly more for
  138. * peace of mind than actual need.
  139. */
  140. public function __destruct() {
  141. $this->colObjects = NULL;
  142. $this->con = NULL;
  143. if ($this->rowCount > 0 ) {
  144. $mess = 'Query destroyed before all rows were saved';
  145. trigger_error($mess, E_USER_WARNING);
  146. };
  147. }// function __destruct
  148. /**
  149. * Magic function to show object when being printed.
  150. *
  151. * The output is formatted as CSV (Comma Separated Values) with a header line
  152. * and string quoted. Note that decimal values are treated like strings and
  153. * blobs are in hexdecminal form with 0x appended but not quoted.
  154. *
  155. * @return string Returns the rows ready to be printed.
  156. */
  157. public function __toString() {
  158. $value = '"' . implode('","', array_keys($this->colTypes)) . '"' . PHP_EOL;
  159. foreach ($this->rows as $row) {
  160. $value .= trim($row, '()') . PHP_EOL;
  161. };
  162. return $value;
  163. }// function __toString ...
  164. /**
  165. * Function used to add row of data to query.
  166. *
  167. * @param array $row Contain assoc array of columns and values to be added to
  168. * query.
  169. *
  170. * @return bool Returns TRUE if row was added, else FALSE.
  171. */
  172. public function addRow($row) {
  173. // Merging defaults with API row should make a complete database record.
  174. $data = array_merge($this->defaults, $row);
  175. $diff = array_diff(array_keys($this->colTypes), array_keys($data));
  176. if (count($diff)) {
  177. $mess = 'Row was missing required fields (' . implode(', ', $diff);
  178. $mess .= ') that are needed for ' . $this->tableName;
  179. trigger_error($mess, E_USER_WARNING);
  180. return FALSE;
  181. };
  182. // Check for extra unknown fields in the data. This should only happen when
  183. // API has changed and the version of Yapeal is out of date.
  184. $diff = array_diff(array_keys($data), array_keys($this->colTypes));
  185. if (count($diff)) {
  186. $mess = 'Row has extra unknown fields (' . implode(', ', $diff);
  187. $mess .= ') that will be ignored for ' . $this->tableName;
  188. trigger_error($mess, E_USER_WARNING);
  189. };
  190. // Make a new array where database fields and API data fields overlap.
  191. $fields = array_intersect(array_keys($this->colTypes), array_keys($data));
  192. $set = array();
  193. foreach ($fields as $field) {
  194. switch ($this->colTypes[$field]) {
  195. case 'C':
  196. case 'D':
  197. case 'N':
  198. case 'T':
  199. case 'X':
  200. // Quote all text, decimal or date type fields.
  201. $set[] = $this->con->qstr($data[$field]);
  202. break;
  203. case 'B':
  204. // If the BLOB is empty use NULL.
  205. if (strlen($data[$field]) == 0) {
  206. $set[] = 'NULL';
  207. // BLOBs need to be converted to hex strings if they aren't already.
  208. } else if ('0x' !== substr($data[$field], 0, 2)) {
  209. $set[] = '0x' . bin2hex($data[$field]);
  210. } else {
  211. $set[] = (string)$data[$field];
  212. };// else '0x' !== substr($row[$field], 0, 2) ...
  213. break;
  214. default:
  215. $set[] = (string)$data[$field];
  216. };// switch $types($field) ...
  217. };// foreach $fields ...
  218. // Put completed row in with the rest.
  219. $this->rows[] = '(' . implode(',', $set) . ')';
  220. // Add row to the row count.
  221. ++$this->rowCount;
  222. // Check if doing auto stores and if there are enough rows do so.
  223. if ($this->autoStore !== FALSE && $this->autoStore == $this->rowCount) {
  224. $this->store();
  225. };// if $this->autoStore !== FALSE ...
  226. return TRUE;
  227. }// function addRow
  228. /**
  229. * Implimentation of count() for countable interface.
  230. *
  231. * @return int Returns count for rows.
  232. */
  233. public function count() {
  234. return $this->rowCount;
  235. }// function count
  236. /**
  237. * Function to access the list of columns and their generic ADO types.
  238. *
  239. * @return array Returns an array of column names and their assocated generic
  240. * ADO types.
  241. */
  242. public function getColumnTypes() {
  243. return $this->colTypes;
  244. }// function getColumnTypes
  245. /**
  246. * Function that will return ADOdb generic data type for an ADOFieldObject.
  247. *
  248. * This is a custom version of the same function available in ADOdb.
  249. *
  250. * @param object $fieldobj An ADOFieldObject to figure out generic type of.
  251. *
  252. * @return string Returns a single character string of the ADOdb generic type.
  253. *
  254. * @throws InvalidArgumentException If $fieldobj isn't an object throws an
  255. * InvalidArgumentException.
  256. */
  257. protected function metaType($fieldobj) {
  258. if (is_object($fieldobj)) {
  259. $t = $fieldobj->type;
  260. $len = $fieldobj->max_length;
  261. } else {
  262. $mess = 'Parameter $fieldobj must be an ADOFieldObject';
  263. trigger_error($mess, E_USER_ERROR);
  264. };// else is_object $fieldobj
  265. switch (strtoupper($t)) {
  266. case 'STRING':
  267. case 'CHAR':
  268. case 'VARCHAR':
  269. case 'TINYBLOB':
  270. case 'TINYTEXT':
  271. case 'ENUM':
  272. case 'SET':
  273. if ($len <= 255) return 'C';
  274. case 'TEXT':
  275. case 'LONGTEXT':
  276. case 'MEDIUMTEXT':
  277. return 'X';
  278. // php_mysql extension always returns 'blob' even if 'text'
  279. // so we have to check whether binary...
  280. case 'IMAGE':
  281. case 'LONGBLOB':
  282. case 'BLOB':
  283. case 'MEDIUMBLOB':
  284. return !empty($fieldobj->binary) ? 'B' : 'X';
  285. case 'YEAR':
  286. case 'DATE':
  287. return 'D';
  288. case 'TIME':
  289. case 'DATETIME':
  290. case 'TIMESTAMP':
  291. return 'T';
  292. case 'INT':
  293. case 'INTEGER':
  294. case 'BIGINT':
  295. case 'TINYINT':
  296. case 'MEDIUMINT':
  297. case 'SMALLINT':
  298. return 'I';
  299. case 'FLOAT':
  300. case 'DOUBLE':
  301. case 'DECIMAL':
  302. case 'DEC':
  303. case 'FIXED':
  304. return 'N';
  305. };// switch strtoupper($t) ...
  306. $mess = 'Unknown ADOFieldObject type in ' . __CLASS__ . PHP_EOL;
  307. $mess .= ' type recieved was ' . $t;
  308. trigger_error($mess, E_USER_ERROR);
  309. }// function metaType
  310. /**
  311. * Used to set default for column.
  312. *
  313. * @param string $name Name of the column.
  314. * @param mixed $value Value to be used as default for column.
  315. *
  316. * @return bool Returns TRUE if column exists in table and default was set.
  317. *
  318. * @throws LogicException Throws LogicException if any rows have already been
  319. * added. All defaults must be set before starting to add data rows.
  320. */
  321. public function setDefault($name, $value) {
  322. if ($this->count() > 0) {
  323. $mess = 'Defaults must be set before any data rows are added';
  324. throw new LogicException($mess, 1);
  325. }
  326. if (!array_key_exists($name, $this->colTypes)) {
  327. $mess = 'Ignoring default for unknown column ' . $name;
  328. $mess .= ' which does not exist in table ' . $this->tableName;
  329. trigger_error($mess, E_USER_WARNING);
  330. return FALSE;
  331. };// if !array_key_exists $name ...
  332. $this->defaults[$name] = $value;
  333. return TRUE;
  334. }// function setDefault
  335. /**
  336. * Used to set defaults for multiple columns.
  337. *
  338. * @param array $defaults List of column names and new default values.
  339. *
  340. * @return bool Returns TRUE if all column defaults could be set, else FALSE.
  341. */
  342. public function setDefaults(array $defaults) {
  343. if(empty($defaults)) {
  344. $mess = 'List must contain as least one column name and value';
  345. trigger_error($mess, E_USER_WARNING);
  346. return FALSE;
  347. }// if empty $defaults ...
  348. $ret = TRUE;
  349. foreach ($defaults as $k => $v) {
  350. if (FALSE === $this->setDefault($k, $v)) {
  351. $ret = FALSE;
  352. };// if !$this->setDefault($k, $v) ...
  353. };// foreach $defaults ...
  354. return $ret;
  355. }// function setDefaults
  356. /**
  357. * Finishes making upsert, empties out rows, then upserts data to database.
  358. *
  359. * @return bool Returns TRUE if upsert worked, else FALSE.
  360. */
  361. public function store() {
  362. if ($this->rowCount == 0) {
  363. $mess = 'No rows to be upsert for ' . $this->tableName;
  364. trigger_error($mess, E_USER_NOTICE);
  365. return FALSE;
  366. };
  367. // Make insert part of upsert.
  368. $sql = 'insert into `' . $this->tableName;
  369. $sql .= '` (`' . implode('`,`', array_keys($this->colTypes)) . '`)';
  370. $sql .= ' values ' . implode(',', $this->rows);
  371. // Insert is now complete don't need rows anymore.
  372. $this->rows = array();
  373. // Keep local copy of row count for transaction check.
  374. $cnt = $this->rowCount;
  375. $this->rowCount = 0;
  376. // Add update part to upsert.
  377. $sql .= ' on duplicate key update ';
  378. // Loop thru and build update.
  379. $updates = array();
  380. foreach (array_keys($this->colTypes) as $k) {
  381. $updates[] = '`' . $k . '`=values(`' . $k . '`)';
  382. };
  383. $sql .= implode(',', $updates);
  384. // Use a transaction for larger upserts to make them faster but fall back to
  385. // normal upsert if transaction fails.
  386. if ($cnt > 10) {
  387. $this->con->StartTrans();
  388. $this->con->Execute($sql);
  389. if (FALSE === $this->con->CompleteTrans()) {
  390. $mess = 'Transaction failed for ' . $this->tableName;
  391. trigger_error($mess, E_USER_WARNING);
  392. } else {
  393. return TRUE;
  394. };// else FALSE === $this->con->CompleteTrans() ...
  395. };// if $this->count() > 10 ...
  396. try {
  397. $this->con->Execute($sql);
  398. }
  399. catch(ADODB_Exception $e) {
  400. $mess = 'Upsert failed for ' . $this->tableName;
  401. trigger_error($mess, E_USER_WARNING);
  402. return FALSE;
  403. }
  404. return TRUE;
  405. }// function store
  406. }
  407. ?>