PageRenderTime 56ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/classes/fORMDatabase.php

https://bitbucket.org/ZilIsiltk/flourish
PHP | 1310 lines | 808 code | 192 blank | 310 comment | 156 complexity | 2da8bb3de3882d8ebcfdb9162548d1a8 MD5 | raw file
  1. <?php
  2. /**
  3. * Holds a single instance of the fDatabase class and provides database manipulation functionality for ORM code
  4. *
  5. * @copyright Copyright (c) 2007-2010 Will Bond, others
  6. * @author Will Bond [wb] <will@flourishlib.com>
  7. * @author Craig Ruksznis, iMarc LLC [cr-imarc] <craigruk@imarc.net>
  8. * @license http://flourishlib.com/license
  9. *
  10. * @package Flourish
  11. * @link http://flourishlib.com/fORMDatabase
  12. *
  13. * @version 1.0.0b27
  14. * @changes 1.0.0b27 Fixed ::addWhereClause() to ignore fuzzy search clauses with no values to match [wb, 2010-10-19]
  15. * @changes 1.0.0b26 Fixed ::insertFromAndGroupByClauses() to handle SQL where a table is references in more than one capitalization [wb, 2010-07-26]
  16. * @changes 1.0.0b25 Fixed ::insertFromAndGroupByClauses() to properly handle recursive relationships [wb, 2010-07-22]
  17. * @changes 1.0.0b24 Fixed ::parseSearchTerms() to work with non-ascii terms [wb, 2010-06-30]
  18. * @changes 1.0.0b23 Fixed error messages in ::retrieve() [wb, 2010-04-23]
  19. * @changes 1.0.0b22 Added support for IBM DB2, fixed an issue with building record sets or records that have recursive relationships [wb, 2010-04-13]
  20. * @changes 1.0.0b21 Changed ::injectFromAndGroupByClauses() to be able to handle table aliases that contain other aliases inside of them [wb, 2010-03-03]
  21. * @changes 1.0.0b20 Fixed a bug where joining to a table two separate ways could cause table alias issues and incorrect SQL to be generated [wb, 2009-12-16]
  22. * @changes 1.0.0b19 Added the ability to compare columns with the `=:`, `!:`, `<:`, `<=:`, `>:` and `>=:` operators [wb, 2009-12-08]
  23. * @changes 1.0.0b18 Fixed a bug affecting where conditions with columns that are not null but have a default value [wb, 2009-11-03]
  24. * @changes 1.0.0b17 Added support for multiple databases [wb, 2009-10-28]
  25. * @changes 1.0.0b16 Internal Backwards Compatibility Break - Renamed methods and significantly changed parameters and functionality for SQL statements to use value placeholders, identifier escaping and to handle schemas [wb, 2009-10-22]
  26. * @changes 1.0.0b15 Streamlined intersection operator SQL and added support for the second value being NULL [wb, 2009-09-21]
  27. * @changes 1.0.0b14 Added support for the intersection operator `><` to ::createWhereClause() [wb, 2009-07-13]
  28. * @changes 1.0.0b13 Added support for the `AND LIKE` operator `&~` to ::createWhereClause() [wb, 2009-07-09]
  29. * @changes 1.0.0b12 Added support for the `NOT LIKE` operator `!~` to ::createWhereClause() [wb, 2009-07-08]
  30. * @changes 1.0.0b11 Added support for concatenated columns to ::escapeBySchema() [cr-imarc, 2009-06-19]
  31. * @changes 1.0.0b10 Updated ::createWhereClause() to properly handle NULLs for arrays of values when doing = and != comparisons [wb, 2009-06-17]
  32. * @changes 1.0.0b9 Changed replacement values in preg_replace() calls to be properly escaped [wb, 2009-06-11]
  33. * @changes 1.0.0b8 Fixed a bug with ::creatingWhereClause() where a null value would not be escaped property [wb, 2009-05-12]
  34. * @changes 1.0.0b7 Fixed a bug where an OR condition in ::createWhereClause() could not have one of the values be an array [wb, 2009-04-22]
  35. * @changes 1.0.0b6 ::insertFromAndGroupByClauses() will no longer wrap ungrouped columns if in a CAST or CASE statement for ORDER BY clauses of queries with a GROUP BY clause [wb, 2009-03-23]
  36. * @changes 1.0.0b5 Fixed ::parseSearchTerms() to include stop words when they are the only thing in the search string [wb, 2008-12-31]
  37. * @changes 1.0.0b4 Fixed a bug where loading a related record in the same table through a one-to-many relationship caused recursion [wb, 2008-12-24]
  38. * @changes 1.0.0b3 Fixed a bug from 1.0.0b2 [wb, 2008-12-05]
  39. * @changes 1.0.0b2 Added support for != and <> to ::createWhereClause() and ::createHavingClause() [wb, 2008-12-04]
  40. * @changes 1.0.0b The initial implementation [wb, 2007-08-04]
  41. */
  42. class fORMDatabase
  43. {
  44. // The following constants allow for nice looking callbacks to static methods
  45. const addHavingClause = 'fORMDatabase::addHavingClause';
  46. const addOrderByClause = 'fORMDatabase::addOrderByClause';
  47. const addPrimaryKeyWhereParams = 'fORMDatabase::addPrimaryKeyWhereParams';
  48. const addWhereClause = 'fORMDatabase::addWhereClause';
  49. const attach = 'fORMDatabase::attach';
  50. const injectFromAndGroupByClauses = 'fORMDatabase::injectFromAndGroupByClauses';
  51. const makeCondition = 'fORMDatabase::makeCondition';
  52. const parseSearchTerms = 'fORMDatabase::parseSearchTerms';
  53. const reset = 'fORMDatabase::reset';
  54. const retrieve = 'fORMDatabase::retrieve';
  55. const splitHavingConditions = 'fORMDatabase::splitHavingConditions';
  56. /**
  57. * An array of fDatabase objects
  58. *
  59. * @var array
  60. */
  61. static private $database_objects = array();
  62. /**
  63. * Allows attaching an fDatabase-compatible objects for by ORM code
  64. *
  65. * If a `$name` other than `default` is used, any fActiveRecord classes
  66. * that should use it will need to be configured by passing the class name
  67. * and `$name` to ::mapClassToDatabase(). The `$name` parameter should be
  68. * unique per database or database master/slave setup.
  69. *
  70. * The `$role` is used by code to allow for master/slave database setups.
  71. * There can only be one database object attached for either of the roles,
  72. * `'read'` or `'write'`. If the role `'both'` is specified, it will
  73. * be applied to both the `'read'` and `'write'` roles. Any sort of logic
  74. * for picking one out of multiple databases should be done before this
  75. * method is called.
  76. *
  77. * @param fDatabase $database An object that is compatible with fDatabase
  78. * @param string $name The name for the database instance
  79. * @param string $role If the database should be used for `'read'`, `'write'` or `'both'` operations
  80. * @return void
  81. */
  82. static public function attach($database, $name='default', $role='both')
  83. {
  84. $valid_roles = array('both', 'write', 'read');
  85. if (!in_array($role, $valid_roles)) {
  86. throw new fProgrammerException(
  87. 'The role specified, %1$s, is invalid. Must be one of: %2$s.',
  88. $role,
  89. join(', ', $valid_roles)
  90. );
  91. }
  92. if (!isset(self::$database_objects[$name])) {
  93. self::$database_objects[$name] = array();
  94. }
  95. settype($role, 'array');
  96. if ($role == array('both')) {
  97. $role = array('write', 'read');
  98. }
  99. foreach ($role as $_role) {
  100. self::$database_objects[$name][$_role] = $database;
  101. }
  102. }
  103. /**
  104. * Translated the where condition for a single column into a SQL clause
  105. *
  106. * @param fDatabase $db The database the query will be run on
  107. * @param fSchema $schema The schema for the database
  108. * @param array $params The parameters for the fDatabase::query() call
  109. * @param string $table The table to create the condition for
  110. * @param string $column The column to store the value in, may also be shorthand column name like `table.column` or `table=>related_table.column`
  111. * @param string $operator Should be `'='`, `'!='`, `'!'`, `'<>'`, `'<'`, `'<='`, `'>'`, `'>='`, `'IN'`, `'NOT IN'`
  112. * @param mixed $values The value(s) to escape
  113. * @param string $escaped_column The escaped column to use in the SQL
  114. * @param string $placeholder This allows overriding the placeholder
  115. * @return array The modified parameters
  116. */
  117. static private function addColumnCondition($db, $schema, $params, $table, $column, $operator, $values, $escaped_column=NULL, $placeholder=NULL)
  118. {
  119. // Some objects when cast to an array turn the members into array keys
  120. if (!is_object($values)) {
  121. settype($values, 'array');
  122. } else {
  123. $values = array($values);
  124. }
  125. // Make sure we have an array with something in it to compare to
  126. if (!$values) { $values = array(NULL); }
  127. // If the table and column specified are real and not some combination
  128. $real_column = $escaped_column === NULL && $placeholder === NULL;
  129. if ($escaped_column === NULL) {
  130. $escaped_column = $db->escape('%r', (strpos($column, '.') === FALSE) ? $table . '.' . $column : $column);
  131. }
  132. list($table, $column) = self::getTableAndColumn($schema, $table, $column);
  133. if ($placeholder === NULL && !in_array($operator, array('=:', '!=:', '<:', '<=:', '>:', '>=:'))) {
  134. $placeholder = $schema->getColumnInfo($table, $column, 'placeholder');
  135. }
  136. // More than one value
  137. if (sizeof($values) > 1) {
  138. switch ($operator) {
  139. case '=':
  140. $non_null_values = array();
  141. $has_null = FALSE;
  142. foreach ($values as $value) {
  143. if ($value === NULL) {
  144. $has_null = TRUE;
  145. continue;
  146. }
  147. $non_null_values[] = $value;
  148. }
  149. if ($has_null) {
  150. $params[0] .= '(' . $escaped_column . ' IS NULL OR ';
  151. }
  152. $params[0] .= $escaped_column . ' IN (' . $placeholder . ')';
  153. $params[] = $non_null_values;
  154. if ($has_null) {
  155. $params[0] .= ')';
  156. }
  157. break;
  158. case '!':
  159. $non_null_values = array();
  160. $has_null = FALSE;
  161. foreach ($values as $value) {
  162. if ($value === NULL) {
  163. $has_null = TRUE;
  164. continue;
  165. }
  166. $non_null_values[] = $value;
  167. }
  168. if ($has_null) {
  169. $params[0] .= '(' . $escaped_column . ' IS NOT NULL AND ';
  170. }
  171. $params[0] .= $escaped_column . ' NOT IN (' . $placeholder . ')';
  172. $params[] = $non_null_values;
  173. if ($has_null) {
  174. $params[0] .= ')';
  175. }
  176. break;
  177. case '~':
  178. $condition = array();
  179. foreach ($values as $value) {
  180. $condition[] = $escaped_column . ' LIKE %s';
  181. $params[] = '%' . $value . '%';
  182. }
  183. $params[0] .= '(' . join(' OR ', $condition) . ')';
  184. break;
  185. case '&~':
  186. $condition = array();
  187. foreach ($values as $value) {
  188. $condition[] = $escaped_column . ' LIKE %s';
  189. $params[] = '%' . $value . '%';
  190. }
  191. $params[0] .= '(' . join(' AND ', $condition) . ')';
  192. break;
  193. case '!~':
  194. $condition = array();
  195. foreach ($values as $value) {
  196. $condition[] = $escaped_column . ' NOT LIKE %s';
  197. $params[] = '%' . $value . '%';
  198. }
  199. $params[0] .= '(' . join(' AND ', $condition) . ')';
  200. break;
  201. default:
  202. throw new fProgrammerException(
  203. 'An invalid array comparison operator, %s, was specified for an array of values',
  204. $operator
  205. );
  206. break;
  207. }
  208. // A single value
  209. } else {
  210. if ($values === array()) {
  211. $value = NULL;
  212. } else {
  213. $value = current($values);
  214. }
  215. switch ($operator) {
  216. case '!:':
  217. $operator = '<>:';
  218. case '=:':
  219. case '<:':
  220. case '<=:':
  221. case '>:':
  222. case '>=:':
  223. $params[0] .= $escaped_column . ' ';
  224. $params[0] .= substr($operator, 0, -1) . ' ';
  225. // If the column to match is a function, split the function
  226. // name off so we can escape the column name
  227. $prefix = '';
  228. $suffix = '';
  229. if (preg_match('#^([^(]+\()\s*([^\s]+)\s*(\))$#D', $value, $parts)) {
  230. $prefix = $parts[1];
  231. $value = $parts[2];
  232. $suffix = $parts[3];
  233. }
  234. $params[0] .= $prefix . $db->escape('%r', (strpos($value, '.') === FALSE) ? $table . '.' . $value : $value) . $suffix;
  235. break;
  236. case '=':
  237. if ($value === NULL) {
  238. $operator = 'IS';
  239. }
  240. $params[0] .= $escaped_column . ' ' . $operator . ' ' . $placeholder;
  241. $params[] = $value;
  242. break;
  243. case '!':
  244. $operator = '<>';
  245. if ($value !== NULL) {
  246. $params[0] .= '(';
  247. } else {
  248. $operator = 'IS NOT';
  249. }
  250. $params[0] .= $escaped_column . ' ' . $operator . ' ' . $placeholder;
  251. $params[] = $value;
  252. if ($value !== NULL) {
  253. $params[0] .= ' OR ' . $escaped_column . ' IS NULL)';
  254. }
  255. break;
  256. case '<':
  257. case '<=':
  258. case '>':
  259. case '>=':
  260. $params[0] .= $escaped_column . ' ' . $operator . ' ' . $placeholder;
  261. $params[] = $value;
  262. break;
  263. case '~':
  264. $params[0] .= $escaped_column . ' LIKE %s';
  265. $params[] = '%' . $value . '%';
  266. break;
  267. case '!~':
  268. $params[0] .= $escaped_column . ' NOT LIKE %s';
  269. $params[] = '%' . $value . '%';
  270. break;
  271. default:
  272. throw new fProgrammerException(
  273. 'An invalid comparison operator, %s, was specified for a single value',
  274. $operator
  275. );
  276. break;
  277. }
  278. }
  279. return $params;
  280. }
  281. /**
  282. * Creates a `HAVING` clause from an array of conditions
  283. *
  284. * @internal
  285. *
  286. * @param fDatabase $db The database the query will be executed on
  287. * @param fSchema $schema The schema for the database
  288. * @param array $params The params for the fDatabase::query() call
  289. * @param string $table The table the query is being executed on
  290. * @param array $conditions The array of conditions - see fRecordSet::build() for format
  291. * @return array The params with the `HAVING` clause added
  292. */
  293. static public function addHavingClause($db, $schema, $params, $table, $conditions)
  294. {
  295. $i = 0;
  296. foreach ($conditions as $expression => $value) {
  297. if ($i) {
  298. $params[0] .= ' AND ';
  299. }
  300. // Splits the operator off of the end of the expression
  301. if (in_array(substr($expression, -3), array('!=:', '>=:', '<=:', '<>:'))) {
  302. $operator = strtr(
  303. substr($expression, -3),
  304. array('<>:' => '!:', '!=:' => '!:')
  305. );
  306. $expression = substr($expression, 0, -3);
  307. } elseif (in_array(substr($expression, -2), array('<=', '>=', '!=', '<>', '=:', '!:', '<:', '>:'))) {
  308. $operator = strtr(
  309. substr($expression, -2),
  310. array('<>' => '!', '!=' => '!')
  311. );
  312. $expression = substr($expression, 0, -2);
  313. } else {
  314. $operator = substr($expression, -1);
  315. $expression = substr($expression, 0, -1);
  316. }
  317. // Quotes the identifier in the expression
  318. preg_match('#^([^(]+\()\s*([^\s]+)\s*(\))$#D', $expression, $parts);
  319. $expression = $parts[1] . $db->escape('%r', $parts[2]) . $parts[3];
  320. // The AVG, SUM and COUNT functions all return a number
  321. $function = strtolower(substr($parts[2], 0, -1));
  322. $placeholder = (in_array($function, array('avg', 'sum', 'count'))) ? '%f' : NULL;
  323. // This removes stray quoting inside of {route} specified for shorthand column names
  324. $expression = preg_replace('#(\{\w+)"\."(\w+\})#', '\1.\2', $expression);
  325. $params = self::addColumnCondition($db, $schema, $params, $table, $parts[2], $operator, $value, $expression, $placeholder);
  326. $i++;
  327. }
  328. return $params;
  329. }
  330. /**
  331. * Adds an `ORDER BY` clause to an array of params for an fDatabase::query() call
  332. *
  333. * @internal
  334. *
  335. * @param fDatabase $db The database the query will be executed on
  336. * @param fSchema $schema The schema object for the database the query will be executed on
  337. * @param array $params The parameters for the fDatabase::query() call
  338. * @param string $table The table any ambigious column references will refer to
  339. * @param array $order_bys The array of order bys to use - see fRecordSet::build() for format
  340. * @return array The params with a SQL `ORDER BY` clause added
  341. */
  342. static public function addOrderByClause($db, $schema, $params, $table, $order_bys)
  343. {
  344. $expressions = array();
  345. foreach ($order_bys as $column => $direction) {
  346. if ((!is_string($column) && !is_object($column) && !is_numeric($column)) || !strlen(trim($column))) {
  347. throw new fProgrammerException(
  348. 'An invalid sort column, %s, was specified',
  349. $column
  350. );
  351. }
  352. $direction = strtoupper($direction);
  353. if (!in_array($direction, array('ASC', 'DESC'))) {
  354. throw new fProgrammerException(
  355. 'An invalid direction, %s, was specified',
  356. $direction
  357. );
  358. }
  359. if (preg_match('#^((?:max|min|avg|sum|count)\()?((?:(?:(?:"?\w+"?\.)?"?\w+(?:\{[\w.]+\})?"?=>)?"?(?:(?:\w+"?\."?)?\w+)(?:\{[\w.]+\})?"?\.)?"?(?:\w+)"?)(?:\))?$#D', $column, $matches)) {
  360. // Parse the expression and get a table and column to determine the data type
  361. list ($clause_table, $clause_column) = self::getTableAndColumn($schema, $table, $matches[2]);
  362. $column_type = $schema->getColumnInfo($clause_table, $clause_column, 'type');
  363. // Make sure each column is qualified with a table name
  364. if (strpos($matches[2], '.') === FALSE) {
  365. $matches[2] = $table . '.' . $matches[2];
  366. }
  367. $matches[2] = $db->escape('%r', $matches[2]);
  368. // Text columns are converted to lowercase for more accurate sorting
  369. if (in_array($column_type, array('varchar', 'char', 'text'))) {
  370. $expression = 'LOWER(' . $matches[2] . ')';
  371. } else {
  372. $expression = $matches[2];
  373. }
  374. // If the column is in an aggregate function, add the function back in
  375. if ($matches[1]) {
  376. $expression = $matches[1] . $expression . ')';
  377. }
  378. $expressions[] = $expression . ' ' . $direction;
  379. } else {
  380. $expressions[] = $column . ' ' . $direction;
  381. }
  382. }
  383. $params[0] .= join(', ', $expressions);
  384. return $params;
  385. }
  386. /**
  387. * Add the appropriate SQL and params for a `WHERE` clause condition for primary keys of the table specified
  388. *
  389. * @internal
  390. *
  391. * @param fSchema $schema The schema for the database the query will be run on
  392. * @param array $params The currently constructed params for fDatabase::query() - the first param should be a SQL statement
  393. * @param string $table The table to build the where clause for
  394. * @param string $table_alias The alias for the table
  395. * @param array &$values The values array for the fActiveRecord object
  396. * @param array &$old_values The old values array for the fActiveRecord object
  397. * @return array The params to pass to fDatabase::query(), including the new primary key where condition
  398. */
  399. static public function addPrimaryKeyWhereParams($schema, $params, $table, $table_alias, &$values, &$old_values)
  400. {
  401. $pk_columns = $schema->getKeys($table, 'primary');
  402. $column_info = $schema->getColumnInfo($table);
  403. $conditions = array();
  404. foreach ($pk_columns as $pk_column) {
  405. $value = fActiveRecord::retrieveOld($old_values, $pk_column, $values[$pk_column]);
  406. // This makes sure the query performs the way an insert will
  407. if ($value === NULL && $column_info[$pk_column]['not_null'] && $column_info[$pk_column]['default'] !== NULL) {
  408. $value = $column_info[$pk_column]['default'];
  409. }
  410. $params[] = $table_alias . '.' . $pk_column;
  411. $params[] = $value;
  412. $conditions[] = self::makeCondition($schema, $table, $pk_column, '=', $value);
  413. }
  414. $params[0] .= join(' AND ', $conditions);
  415. return $params;
  416. }
  417. /**
  418. * Adds a `WHERE` clause, from an array of conditions, to the parameters for an fDatabase::query() call
  419. *
  420. * @internal
  421. *
  422. * @param fDatabase $db The database the query will be executed on
  423. * @param fSchema $schema The schema for the database
  424. * @param array $params The parameters for the fDatabase::query() call
  425. * @param string $table The table any ambigious column references will refer to
  426. * @param array $conditions The array of conditions - see fRecordSet::build() for format
  427. * @return array The params with the SQL `WHERE` clause added
  428. */
  429. static public function addWhereClause($db, $schema, $params, $table, $conditions)
  430. {
  431. $i = 0;
  432. foreach ($conditions as $column => $values) {
  433. if ($i) {
  434. $params[0] .= ' AND ';
  435. }
  436. if (in_array(substr($column, -3), array('!=:', '>=:', '<=:', '<>:'))) {
  437. $operator = strtr(
  438. substr($column, -3),
  439. array('<>:' => '!:', '!=:' => '!:')
  440. );
  441. $column = substr($column, 0, -3);
  442. } elseif (in_array(substr($column, -2), array('<=', '>=', '!=', '<>', '!~', '&~', '><', '=:', '!:', '<:', '>:'))) {
  443. $operator = strtr(
  444. substr($column, -2),
  445. array('<>' => '!', '!=' => '!')
  446. );
  447. $column = substr($column, 0, -2);
  448. } else {
  449. $operator = substr($column, -1);
  450. $column = substr($column, 0, -1);
  451. }
  452. if (!is_object($values)) {
  453. settype($values, 'array');
  454. } else {
  455. $values = array($values);
  456. }
  457. if (!$values) { $values = array(NULL); }
  458. $new_values = array();
  459. foreach ($values as $value) {
  460. if (is_object($value) && is_callable(array($value, '__toString'))) {
  461. $value = $value->__toString();
  462. } elseif (is_object($value)) {
  463. $value = (string) $value;
  464. }
  465. $new_values[] = $value;
  466. }
  467. $values = $new_values;
  468. // Multi-column condition
  469. if (preg_match('#(?<!\|)\|(?!\|)#', $column)) {
  470. $columns = explode('|', $column);
  471. $operators = array();
  472. foreach ($columns as &$_column) {
  473. if (in_array(substr($_column, -3), array('!=:', '>=:', '<=:', '<>:'))) {
  474. $operators[] = strtr(
  475. substr($_column, -3),
  476. array('<>:' => '!:', '!=:' => '!:')
  477. );
  478. $_column = substr($_column, 0, -3);
  479. } elseif (in_array(substr($_column, -2), array('<=', '>=', '!=', '<>', '!~', '&~', '=:', '!:', '<:', '>:'))) {
  480. $operators[] = strtr(
  481. substr($_column, -2),
  482. array('<>' => '!', '!=' => '!')
  483. );
  484. $_column = substr($_column, 0, -2);
  485. } elseif (!ctype_alnum(substr($_column, -1))) {
  486. $operators[] = substr($_column, -1);
  487. $_column = substr($_column, 0, -1);
  488. }
  489. }
  490. $operators[] = $operator;
  491. if (sizeof($operators) == 1) {
  492. // Make sure every column is qualified by a table name
  493. $new_columns = array();
  494. foreach ($columns as $column) {
  495. if (strpos($column, '.') === FALSE) {
  496. $column = $table . '.' . $column;
  497. }
  498. $new_columns[] = $column;
  499. }
  500. $columns = $new_columns;
  501. // Handle fuzzy searches
  502. if ($operator == '~') {
  503. // If the value to search is a single string value, parse it for search terms
  504. if (sizeof($values) == 1 && is_string($values[0])) {
  505. $values = self::parseSearchTerms($values[0], TRUE);
  506. }
  507. // Skip fuzzy matches with no values to match
  508. if ($values === array()) {
  509. $params[0] .= ' 1 = 1 ';
  510. continue;
  511. }
  512. $condition = array();
  513. foreach ($values as $value) {
  514. $sub_condition = array();
  515. foreach ($columns as $column) {
  516. $sub_condition[] = $db->escape('%r', $column) . ' LIKE %s';
  517. $params[] = '%' . $value . '%';
  518. }
  519. $condition[] = '(' . join(' OR ', $sub_condition) . ')';
  520. }
  521. $params[0] .= ' (' . join(' AND ', $condition) . ') ';
  522. // Handle intersection
  523. } elseif ($operator == '><') {
  524. if (sizeof($columns) != 2 || sizeof($values) != 2) {
  525. throw new fProgrammerException(
  526. 'The intersection operator, %s, requires exactly two columns and two values',
  527. $operator
  528. );
  529. }
  530. $escaped_columns = array(
  531. $db->escape('%r', $columns[0]),
  532. $db->escape('%r', $columns[1])
  533. );
  534. list($column_1_table, $column_1) = self::getTableAndColumn($schema, $table, $columns[0]);
  535. list($column_2_table, $column_2) = self::getTableAndColumn($schema, $table, $columns[1]);
  536. $placeholders = array(
  537. $schema->getColumnInfo($column_1_table, $column_1, 'placeholder'),
  538. $schema->getColumnInfo($column_2_table, $column_2, 'placeholder')
  539. );
  540. if ($values[1] === NULL) {
  541. $part_1 = '(' . $escaped_columns[1] . ' IS NULL AND ' . $escaped_columns[0] . ' = ' . $placeholders[0] . ')';
  542. $part_2 = '(' . $escaped_columns[1] . ' IS NOT NULL AND ' . $escaped_columns[0] . ' <= ' . $placeholders[0] . ' AND ' . $escaped_columns[1] . ' >= ' . $placeholders[1] . ')';
  543. $params[] = $values[0];
  544. $params[] = $values[0];
  545. $params[] = $values[0];
  546. } else {
  547. $part_1 = '(' . $escaped_columns[0] . ' <= ' . $placeholders[0] . ' AND ' . $escaped_columns[1] . ' >= ' . $placeholders[1] . ')';
  548. $part_2 = '(' . $escaped_columns[0] . ' >= ' . $placeholders[0] . ' AND ' . $escaped_columns[0] . ' <= ' . $placeholders[0] . ')';
  549. $params[] = $values[0];
  550. $params[] = $values[0];
  551. $params[] = $values[0];
  552. $params[] = $values[1];
  553. }
  554. $params[0] .= ' (' . $part_1 . ' OR ' . $part_2 . ') ';
  555. } else {
  556. throw new fProgrammerException(
  557. 'An invalid comparison operator, %s, was specified for multiple columns',
  558. $operator
  559. );
  560. }
  561. // Handle OR combos
  562. } else {
  563. if (sizeof($columns) != sizeof($values)) {
  564. throw new fProgrammerException(
  565. 'When creating an %1$s where clause there must be an equal number of columns and values, however there are not',
  566. 'OR',
  567. sizeof($columns),
  568. sizeof($values)
  569. );
  570. }
  571. if (sizeof($columns) != sizeof($operators)) {
  572. throw new fProgrammerException(
  573. 'When creating an %s where clause there must be a comparison operator for each column, however one or more is missing',
  574. 'OR'
  575. );
  576. }
  577. $params[0] .= ' (';
  578. $iterations = sizeof($columns);
  579. for ($j=0; $j<$iterations; $j++) {
  580. if ($j) {
  581. $params[0] .= ' OR ';
  582. }
  583. $params = self::addColumnCondition($db, $schema, $params, $table, $columns[$j], $operators[$j], $values[$j]);
  584. }
  585. $params[0] .= ') ';
  586. }
  587. // Concatenated columns
  588. } elseif (strpos($column, '||') !== FALSE) {
  589. $parts = explode('||', $column);
  590. $new_parts = array();
  591. foreach ($parts as $part) {
  592. $part = trim($part);
  593. if ($part[0] != "'") {
  594. $new_parts[] = $db->escape('%r', $part);
  595. } else {
  596. $new_parts[] = $part;
  597. }
  598. }
  599. $escaped_column = join('||', $new_parts);
  600. $params = self::addColumnCondition($db, $schema, $params, $table, $column, $operator, $values, $escaped_column, '%s');
  601. // Single column condition
  602. } else {
  603. $params = self::addColumnCondition($db, $schema, $params, $table, $column, $operator, $values);
  604. }
  605. $i++;
  606. }
  607. return $params;
  608. }
  609. /**
  610. * Takes a table name, cleans off quoting and removes the schema name if unambiguous
  611. *
  612. * @param fSchema $schema The schema object for the database being inspected
  613. * @param string $table The table name to be made cleaned
  614. * @return string The cleaned table name
  615. */
  616. static private function cleanTableName($schema, $table)
  617. {
  618. $table = str_replace('"', '', $table);
  619. $tables = array_flip($schema->getTables());
  620. if (!isset($tables[$table])) {
  621. $short_table = preg_replace('#^\w\.#', '', $table);
  622. if (isset($tables[$short_table])) {
  623. $table = $short_table;
  624. }
  625. }
  626. return strtolower($table);
  627. }
  628. /**
  629. * Creates a `FROM` clause from a join array
  630. *
  631. * @internal
  632. *
  633. * @param fDatabase $db The database the query will be run on
  634. * @param array $joins The joins to create the `FROM` clause out of
  635. * @return string The from clause (does not include the word `FROM`)
  636. */
  637. static private function createFromClauseFromJoins($db, $joins)
  638. {
  639. $sql = '';
  640. foreach ($joins as $join) {
  641. // Here we handle the first table in a join
  642. if ($join['join_type'] == 'none') {
  643. $sql .= $db->escape('%r', $join['table_name']);
  644. if ($join['table_alias'] != $join['table_name']) {
  645. $sql .= ' ' . $db->escape('%r', $join['table_alias']);
  646. }
  647. // Here we handle all other joins
  648. } else {
  649. $sql .= ' ' . strtoupper($join['join_type']) . ' ' . $db->escape('%r', $join['table_name']);
  650. if ($join['table_alias'] != $join['table_name']) {
  651. $sql .= ' ' . $db->escape('%r', $join['table_alias']);
  652. }
  653. if (!empty($join['on_clause_fields'])) {
  654. $sql .= ' ON ' . $db->escape('%r', $join['on_clause_fields'][0]) . ' = ' . $db->escape('%r', $join['on_clause_fields'][1]);
  655. }
  656. }
  657. }
  658. return $sql;
  659. }
  660. /**
  661. * Creates join information for the table shortcut provided
  662. *
  663. * @internal
  664. *
  665. * @param fSchema $schema The schema object for the tables/joins
  666. * @param string $table The primary table
  667. * @param string $table_alias The primary table alias
  668. * @param string $related_table The related table
  669. * @param string $route The route to the related table
  670. * @param array &$joins The names of the joins that have been created
  671. * @param array &$used_aliases The aliases that have been used
  672. * @return string The name of the significant join created
  673. */
  674. static private function createJoin($schema, $table, $table_alias, $related_table, $route, &$joins, &$used_aliases)
  675. {
  676. $routes = fORMSchema::getRoutes($schema, $table, $related_table);
  677. if (!isset($routes[$route])) {
  678. throw new fProgrammerException(
  679. 'An invalid route, %1$s, was specified for the relationship from %2$s to %3$s',
  680. $route,
  681. $table,
  682. $related_table
  683. );
  684. }
  685. if (isset($joins[$table . '_' . $related_table . '{' . $route . '}'])) {
  686. return $table . '_' . $related_table . '{' . $route . '}';
  687. }
  688. // If the route uses a join table
  689. if (isset($routes[$route]['join_table'])) {
  690. $join = array(
  691. 'join_type' => 'LEFT JOIN',
  692. 'table_name' => $routes[$route]['join_table'],
  693. 'table_alias' => self::createNewAlias($routes[$route]['join_table'], $used_aliases),
  694. 'on_clause_fields' => array()
  695. );
  696. $join2 = array(
  697. 'join_type' => 'LEFT JOIN',
  698. 'table_name' => $related_table,
  699. 'table_alias' => self::createNewAlias($related_table, $used_aliases),
  700. 'on_clause_fields' => array()
  701. );
  702. if ($table != $related_table) {
  703. $join['on_clause_fields'][] = $table_alias . '.' . $routes[$route]['column'];
  704. $join['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['join_column'];
  705. $join2['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['join_related_column'];
  706. $join2['on_clause_fields'][] = $join2['table_alias'] . '.' . $routes[$route]['related_column'];
  707. } else {
  708. $join['on_clause_fields'][] = $table_alias . '.' . $routes[$route]['column'];
  709. $join['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['join_related_column'];
  710. $join2['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['join_column'];
  711. $join2['on_clause_fields'][] = $join2['table_alias'] . '.' . $routes[$route]['related_column'];
  712. }
  713. $joins[$table . '_' . $related_table . '{' . $route . '}_join'] = $join;
  714. $joins[$table . '_' . $related_table . '{' . $route . '}'] = $join2;
  715. // If the route is a direct join
  716. } else {
  717. $join = array(
  718. 'join_type' => 'LEFT JOIN',
  719. 'table_name' => $related_table,
  720. 'table_alias' => self::createNewAlias($related_table, $used_aliases),
  721. 'on_clause_fields' => array()
  722. );
  723. if ($table != $related_table) {
  724. $join['on_clause_fields'][] = $table_alias . '.' . $routes[$route]['column'];
  725. $join['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['related_column'];
  726. } else {
  727. $join['on_clause_fields'][] = $table_alias . '.' . $routes[$route]['related_column'];
  728. $join['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['column'];
  729. }
  730. $joins[$table . '_' . $related_table . '{' . $route . '}'] = $join;
  731. }
  732. return $table . '_' . $related_table . '{' . $route . '}';
  733. }
  734. /**
  735. * Creates a new table alias
  736. *
  737. * @internal
  738. *
  739. * @param string $table The table to create an alias for
  740. * @param array &$used_aliases The aliases that have been used
  741. * @return string The alias to use for the table
  742. */
  743. static private function createNewAlias($table, &$used_aliases)
  744. {
  745. if (!in_array($table, $used_aliases)) {
  746. $used_aliases[] = $table;
  747. return $table;
  748. }
  749. // This will strip any schema name off the beginning
  750. $table = preg_replace('#^\w+\.#', '', $table);
  751. $i = 1;
  752. while(in_array($table . $i, $used_aliases)) {
  753. $i++;
  754. }
  755. $used_aliases[] = $table . $i;
  756. return $table . $i;
  757. }
  758. /**
  759. * Gets the table and column name from a shorthand column name
  760. *
  761. * @param fSchema $schema The schema for the database
  762. * @param string $table The table to use when no table is specified in the shorthand
  763. * @param string $column The shorthand column definition - see fRecordSet::build() for possible syntaxes
  764. * @return array The $table and $column, suitable for use with fSchema
  765. */
  766. static private function getTableAndColumn($schema, $table, $column)
  767. {
  768. // Handle shorthand column names like table.column and table=>related_table.column
  769. if (preg_match('#((?:"?\w+"?\.)?"?\w+)(?:\{[\w.]+\})?"?\."?(\w+)"?$#D', $column, $match)) {
  770. $table = $match[1];
  771. $column = $match[2];
  772. }
  773. $table = self::cleanTableName($schema, $table);
  774. $column = str_replace('"', '', $column);
  775. return array($table, $column);
  776. }
  777. /**
  778. * Finds all of the table names in the SQL and creates the appropriate `FROM` and `GROUP BY` clauses with all necessary joins
  779. *
  780. * The SQL string should contain two placeholders, `:from_clause` and
  781. * `:group_by_clause`, although the later may be omitted if necessary. All
  782. * columns should be qualified with their full table name.
  783. *
  784. * Here is an example SQL string to pass in presumming that the tables
  785. * users and groups are in a relationship:
  786. *
  787. * {{{
  788. * SELECT users.* FROM :from_clause WHERE groups.group_id = 5 :group_by_clause ORDER BY lower(users.first_name) ASC
  789. * }}}
  790. *
  791. * @internal
  792. *
  793. * @param fDatabase $db The database the query is to be executed on
  794. * @param fSchema $schema The schema for the database
  795. * @param array $params The parameters for the fDatabase::query() call
  796. * @param string $table The main table to be queried
  797. * @return array The params with the SQL `FROM` and `GROUP BY` clauses injected
  798. */
  799. static public function injectFromAndGroupByClauses($db, $schema, $params, $table)
  800. {
  801. $table = self::cleanTableName($schema, $table);
  802. $joins = array();
  803. if (strpos($params[0], ':from_clause') === FALSE) {
  804. throw new fProgrammerException(
  805. 'No %1$s placeholder was found in:%2$s',
  806. ':from_clause',
  807. "\n" . $params[0]
  808. );
  809. }
  810. $has_group_by_placeholder = (strpos($params[0], ':group_by_clause') !== FALSE) ? TRUE : FALSE;
  811. // Separate the SQL from quoted values
  812. preg_match_all("#(?:'(?:''|\\\\'|\\\\[^']|[^'\\\\])*')|(?:[^']+)#", $params[0], $matches);
  813. $table_alias = $table;
  814. $used_aliases = array();
  815. $table_map = array();
  816. // If we are not passing in existing joins, start with the specified table
  817. if (!$joins) {
  818. $joins[] = array(
  819. 'join_type' => 'none',
  820. 'table_name' => $table,
  821. 'table_alias' => $table_alias
  822. );
  823. }
  824. $used_aliases[] = $table_alias;
  825. foreach ($matches[0] as $match) {
  826. if ($match[0] != "'") {
  827. // This removes quotes from around . in the {route} specified of a shorthand column name
  828. $match = preg_replace('#(\{\w+)"\."(\w+\})#', '\1.\2', $match);
  829. preg_match_all('#(?<!\w|"|=>)((?:"?((?:\w+"?\."?)?\w+)(?:\{([\w.]+)\})?"?=>)?("?(?:\w+"?\."?)?\w+)(?:\{([\w.]+)\})?"?)\."?\w+"?(?=[^\w".{])#m', $match, $table_matches, PREG_SET_ORDER);
  830. foreach ($table_matches as $table_match) {
  831. if (!isset($table_match[5])) {
  832. $table_match[5] = NULL;
  833. }
  834. if (!empty($table_match[2])) {
  835. $table_match[2] = self::cleanTableName($schema, $table_match[2]);
  836. }
  837. $table_match[4] = self::cleanTableName($schema, $table_match[4]);
  838. if (in_array($db->getType(), array('oracle', 'db2'))) {
  839. foreach (array(2, 3, 4, 5) as $subpattern) {
  840. if (isset($table_match[$subpattern])) {
  841. $table_match[$subpattern] = strtolower($table_match[$subpattern]);
  842. }
  843. }
  844. }
  845. // This is a related table that is going to join to a once-removed table
  846. if (!empty($table_match[2])) {
  847. $related_table = $table_match[2];
  848. $route = fORMSchema::getRouteName($schema, $table, $related_table, $table_match[3]);
  849. $join_name = $table . '_' . $related_table . '{' . $route . '}';
  850. $once_removed_table = $table_match[4];
  851. // Add the once removed table to the aliases in case we also join directly to it
  852. // which may cause the replacements later in this method to convert first to the
  853. // real table name and then from the real table to the real table's alias
  854. if (!in_array($once_removed_table, $used_aliases)) {
  855. $used_aliases[] = $once_removed_table;
  856. }
  857. self::createJoin($schema, $table, $table_alias, $related_table, $route, $joins, $used_aliases);
  858. $route = fORMSchema::getRouteName($schema, $related_table, $once_removed_table, $table_match[5]);
  859. $join_name = self::createJoin($schema, $related_table, $joins[$join_name]['table_alias'], $once_removed_table, $route, $joins, $used_aliases);
  860. $table_map[$table_match[1]] = $db->escape('%r', $joins[$join_name]['table_alias']);
  861. // Remove the once removed table from the aliases so we also join directly to it without an alias
  862. unset($used_aliases[array_search($once_removed_table, $used_aliases)]);
  863. // This is a related table
  864. } elseif (($table_match[4] != $table || fORMSchema::getRoutes($schema, $table, $table_match[4])) && self::cleanTableName($schema, $table_match[1]) != $table) {
  865. $related_table = $table_match[4];
  866. $route = fORMSchema::getRouteName($schema, $table, $related_table, $table_match[5]);
  867. $join_name = self::createJoin($schema, $table, $table_alias, $related_table, $route, $joins, $used_aliases);
  868. $table_map[$table_match[1]] = $db->escape('%r', $joins[$join_name]['table_alias']);
  869. }
  870. }
  871. }
  872. }
  873. // Determine if we joined a *-to-many relationship
  874. $joined_to_many = FALSE;
  875. foreach ($joins as $name => $join) {
  876. if (is_numeric($name)) {
  877. continue;
  878. }
  879. // Many-to-many uses a join table
  880. if (substr($name, -5) == '_join') {
  881. $joined_to_many = TRUE;
  882. break;
  883. }
  884. $main_table = preg_replace('#_' . $join['table_name'] . '{\w+}$#iD', '', $name);
  885. $second_table = $join['table_name'];
  886. $route = preg_replace('#^[^{]+{([\w.]+)}$#D', '\1', $name);
  887. $routes = fORMSchema::getRoutes($schema, $main_table, $second_table, '*-to-many');
  888. if (isset($routes[$route])) {
  889. $joined_to_many = TRUE;
  890. break;
  891. }
  892. }
  893. $found_order_by = FALSE;
  894. $from_clause = self::createFromClauseFromJoins($db, $joins);
  895. // If we are joining on a *-to-many relationship we need to group by the
  896. // columns in the main table to prevent duplicate entries
  897. if ($joined_to_many) {
  898. $column_info = $schema->getColumnInfo($table);
  899. $columns = array();
  900. foreach ($column_info as $column => $info) {
  901. $columns[] = $table . '.' . $column;
  902. }
  903. $group_by_columns = $db->escape('%r ', $columns);
  904. $group_by_clause = ' GROUP BY ' . $group_by_columns;
  905. } else {
  906. $group_by_clause = ' ';
  907. $group_by_columns = '';
  908. }
  909. // Put the SQL back together
  910. $new_sql = '';
  911. $preg_table_pattern = preg_quote($table, '#') . '\.|' . preg_quote('"' . $table . '"', '#') . '\.';
  912. foreach ($matches[0] as $match) {
  913. $temp_sql = $match;
  914. // Get rid of the => notation and the :from_clause placeholder
  915. if ($match[0] !== "'") {
  916. // This removes quotes from around . in the {route} specified of a shorthand column name
  917. $temp_sql = preg_replace('#(\{\w+)"\."(\w+\})#', '\1.\2', $match);
  918. foreach ($table_map as $arrow_table => $alias) {
  919. $temp_sql = preg_replace('#(?<![\w"])' . preg_quote($arrow_table, '#') . '(?!=[\w"])#', $alias, $temp_sql);
  920. }
  921. // In the ORDER BY clause we need to wrap columns in
  922. if ($found_order_by && $joined_to_many) {
  923. $temp_sql = preg_replace('#(?<!avg\(|count\(|max\(|min\(|sum\(|cast\(|case |when |"|avg\("|count\("|max\("|min\("|sum\("|cast\("|case "|when "|\{)\b((?!' . $preg_table_pattern . ')("?\w+"?\.)?"?\w+"?\."?\w+"?)(?![^\w."])#i', 'max(\1)', $temp_sql);
  924. }
  925. if ($joined_to_many && preg_match('#order\s+by#i', $temp_sql)) {
  926. $order_by_found = TRUE;
  927. $parts = preg_split('#(order\s+by)#i', $temp_sql, -1, PREG_SPLIT_DELIM_CAPTURE);
  928. $parts[2] = $temp_sql = preg_replace('#(?<!avg\(|count\(|max\(|min\(|sum\(|cast\(|case |when |"|avg\("|count\("|max\("|min\("|sum\("|cast\("|case "|when "|\{)\b((?!' . $preg_table_pattern . ')("?\w+"?\.)?"?\w+"?\."?\w+"?)(?![^\w."])#i', 'max(\1)', $parts[2]);
  929. $temp_sql = join('', $parts);
  930. }
  931. $temp_sql = str_replace(':from_clause', $from_clause, $temp_sql);
  932. if ($has_group_by_placeholder) {
  933. $temp_sql = preg_replace('#\s:group_by_clause(\s|$)#', strtr($group_by_clause, array('\\' => '\\\\', '$' => '\\$')), $temp_sql);
  934. } elseif ($group_by_columns) {
  935. $temp_sql = preg_replace('#(\sGROUP\s+BY\s((?!HAVING|ORDER\s+BY).)*)\s#i', '\1, ' . strtr($group_by_columns, array('\\' => '\\\\', '$' => '\\$')), $temp_sql);
  936. }
  937. }
  938. $new_sql .= $temp_sql;
  939. }
  940. $params[0] = $new_sql;
  941. return $params;
  942. }
  943. /**
  944. * Makes a condition for a SQL statement out of fDatabase::escape() placeholders
  945. *
  946. * @internal
  947. *
  948. * @param fSchema $schema The schema object for the database the query will be executed on
  949. * @param string $table The table to create the condition for
  950. * @param string $column The column to make the condition for
  951. * @param string $comparison_operator The comparison operator for the condition
  952. * @param mixed $value The value for the condition, which allows the $comparison_operator to be tweaked for NULL values
  953. * @return string A SQL condition using fDatabase::escape() placeholders
  954. */
  955. static public function makeCondition($schema, $table, $column, $comparison_operator, $value)
  956. {
  957. list($table, $column) = self::getTableAndColumn($schema, $table, $column);
  958. $co = strtr($comparison_operator, array('!' => '<>', '!=' => '<>'));
  959. if ($value === NULL) {
  960. if (in_array(trim($co), array('=', 'IN'))) {
  961. $co = 'IS';
  962. } elseif (in_array(trim($co), array('<>', 'NOT IN'))) {
  963. $co = 'IS NOT';
  964. }
  965. }
  966. return '%r ' . $co . ' ' . $schema->getColumnInfo($table, $column, 'placeholder');
  967. }
  968. /**
  969. * Parses a search string into search terms, supports quoted phrases and removes extra punctuation
  970. *
  971. * @internal
  972. *
  973. * @param string $terms A text string from a form input to parse into search terms
  974. * @param boolean $ignore_stop_words If stop words should be ignored, this setting will be ignored if all words are stop words
  975. * @return void
  976. */
  977. static public function parseSearchTerms($terms, $ignore_stop_words=FALSE)
  978. {
  979. $stop_words = array(
  980. 'i', 'a', 'an', 'are', 'as', 'at', 'be',
  981. 'by', 'de', 'en', 'en', 'for', 'from', 'how',
  982. 'in', 'is', 'it', 'la', 'of', 'on', 'or',
  983. 'that', 'the', 'this', 'to', 'was', 'what', 'when',
  984. 'where', 'who', 'will'
  985. );
  986. preg_match_all('#(?:"[^"]+"|[^\s]+)#', $terms, $matches);
  987. $good_terms = array();
  988. $ignored_terms = array();
  989. foreach ($matches[0] as $match) {
  990. // Remove phrases from quotes
  991. if ($match[0] == '"' && substr($match, -1)) {
  992. $match = substr($match, 1, -1);
  993. // Trim any punctuation off of the beginning and end of terms
  994. } else {
  995. $match = preg_replace('#(^[\pC\pC\pM\pP\pS\pZ]+|[\pC\pC\pM\pP\pS\pZ]+$)#iDu', '', $match);
  996. }
  997. if ($ignore_stop_words && in_array(strtolower($match), $stop_words)) {
  998. $ignored_terms[] = $match;
  999. continue;
  1000. }
  1001. $good_terms[] = $match;
  1002. }
  1003. // If no terms were parsed, that means all words were stop words
  1004. if ($ignored_terms && !$good_terms) {
  1005. $good_terms = $ignored_terms;
  1006. }
  1007. return $good_terms;
  1008. }
  1009. /**
  1010. * Resets the configuration of the class
  1011. *
  1012. * @internal
  1013. *
  1014. * @return void
  1015. */
  1016. static public function reset()
  1017. {
  1018. self::$database_objects = array();
  1019. }
  1020. /**
  1021. * Return the instance of the fDatabase class
  1022. *
  1023. * @param string $class The class to retrieve the database for - if not specified, the default database will be returned
  1024. * @param string $role If the database will be used for `'write'`, `'read'` or `'either'` operations
  1025. * @return fDatabase The database instance
  1026. */
  1027. static public function retrieve($class='fActiveRecord', $role='either')
  1028. {
  1029. if (substr($class, 0, 5) == 'name:') {
  1030. $database_name = substr($class, 5);
  1031. } else {
  1032. $database_name = fORM::getDatabaseName($class);
  1033. }
  1034. if (!isset(self::$database_objects[$database_name])) {
  1035. throw new fProgrammerException(
  1036. 'The database object named "%1$s" has not been attached via %2$s yet',
  1037. $database_name,
  1038. __CLASS__ . '::attach()'
  1039. );
  1040. }
  1041. if ($role == 'write' || $role == 'read') {
  1042. // If the user wants a read database but we are in a transaction on the write database, return
  1043. // the write database to allow for comparing data changed since the transaction started
  1044. if ($role == 'read' && isset(self::$database_objects[$database_name]['write']) && self::$database_objects[$database_name]['write']->isInsideTransaction()) {
  1045. $role = 'write';
  1046. }
  1047. if (!isset(self::$database_objects[$database_name][$role])) {
  1048. throw new fProgrammerException(
  1049. 'The database object named "%1$s" for the %s$2 role has not been attached via %3$s yet',
  1050. $database_name,
  1051. $role,
  1052. __CLASS__ . '::attach()'
  1053. );
  1054. }
  1055. return self::$database_objects[$database_name][$role];
  1056. }
  1057. if (isset(self::$database_objects[$database_name]['write'])) {
  1058. return self::$database_objects[$database_name]['write'];
  1059. } elseif (isset(self::$database_objects[$database_name]['read'])) {
  1060. return self::$database_objects[$database_name]['read'];
  1061. }
  1062. }
  1063. /**
  1064. * Removed aggregate function calls from where conditions array and puts them in a having conditions array
  1065. *
  1066. * @internal
  1067. *
  1068. * @param array &$where_conditions The where conditions to look through for aggregate functions
  1069. * @return array The conditions to be put in a `HAVING` clause
  1070. */
  1071. static public function splitHavingConditions(&$where_conditions)
  1072. {
  1073. $having_conditions = array();
  1074. foreach ($where_conditions as $column => $value) {
  1075. $column_has_aggregate = preg_match('#^(count\(|max\(|avg\(|min\(|sum\()#i', $column);
  1076. $is_column_compare_with_aggregate = substr($column, -1) == ':' && preg_match('#^(count\(|max\(|avg\(|min\(|sum\()#i', $value);
  1077. if ($column_has_aggregate || $is_column_compare_with_aggregate) {
  1078. $having_conditions[$column] = $value;
  1079. unset($where_conditions[$column]);
  1080. }
  1081. }
  1082. return $having_conditions;
  1083. }
  1084. /**
  1085. * Forces use as a static class
  1086. *
  1087. * @return fORMDatabase
  1088. */
  1089. private function __construct() { }
  1090. }
  1091. /**
  1092. * Copyright (c) 2007-2010 Will Bond <will@flourishlib.com>, others
  1093. *
  1094. * Permission is hereby granted, free of charge, to any person obtaining a copy
  1095. * of this software and associated documentation files (the "Software"), to deal
  1096. * in the Software without restriction, including without limitation the rights
  1097. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  1098. * copies of the Software, and to permit persons to whom the Software is
  1099. * furnished to do so, subject to the following conditions:
  1100. *
  1101. * The above copyright notice and this permission notice shall be included in
  1102. * all copies or substantial portions of the Software.
  1103. *
  1104. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  1105. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  1106. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  1107. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  1108. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  1109. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  1110. * THE SOFTWARE.
  1111. */