* @package WB * @subpackage db */ WBClass::load('WBDatasource_Callback' , 'WBLog' , 'WBString'); /** * Simple table access * * @version 0.5.3 * @package WB * @subpackage db */ class WBDatasource_SQLCommon extends WBStdClass { /** * The absulut maximum number for LIMIT * As this limit is too big for even big integers, it must be a string! * This hack is required to satify MySQL 4.1 :-/ */ const MAX_LIMT = '18446744073709551614'; const KEYMIN = 1000; /** * list of known tables * @var array */ protected $tables = array(); /** * translate table date * @var bool */ private $doTranslation = true; /** * trigger events * @var bool */ private $doTriggerEvent = true; /** * Prefix for triggered events * @var string */ protected $eventPrefix = 'sqlcommon'; /** * empty constructor for future user * * @param unknown_type $parameter */ public function __construct( $parameter = array() ) { } /** * Switch on/off translation feature * * @param bool $switch * @deprecated because of typo :-( */ public function switchTranlsation($switch = false) { $this->switchTranslation($switch); } /** * Switch on/off translation feature * * @param bool $switch */ public function switchTranslation($switch = false) { $this->doTranslation = $switch; } /** * Switch on/off event trigger * * @param bool $switch */ public function switchEventTrigger($switch = false) { $this->doTriggerEvent = $switch; } /** * Translate row from result * * Check translation-config of table and translate translatable columns * * @see switchTranlsation() * @param string $table * @param array $row */ protected function translate($table, &$row) { if (!$this->doTranslation) { return; } $c = $this->getTableInfo($table); if (!isset($c['translatable']) || !is_array($c['translatable']) || empty($c['translatable'])) { return; } $domain = null; if (!empty($c['translationdomain'])) { $domain = $c['translationdomain']; } foreach ($c['translatable'] as $trans) { if (empty($row[$trans])) { continue; } $row[$trans] = patI18n::dgettext($domain, $row[$trans]); } } /** * load table definition * * Parameter: * - "tablelist": config file which contains the list of known tables. * * This class utilizes @link WBDatasource_SQL this implies that you may * use all constructor parameter of this class, as well. * * @param array $parameter */ public function loadTables( $parameter = array() ) { $conf = WBClass::create( 'WBConfig' ); /** @var WBConfig $conf */ $conf->load('config'); $param = $conf->get('db'); if (isset($parameter['tablelist'])) { $param['tablelist'] = $parameter['tablelist']; } if (is_array($param['tablelist'])) { $this->setTables($param['tablelist']); return; } $conf->load($param['tablelist']); $this->setTables($conf->get()); } /** * configure list of tables * * @param array $conf list of tables with configuration */ public function setTables( $conf ) { foreach( $conf as $t => $c ) { if( isset( $this->tables[$t] ) ) { $this->tables[$t] = array_merge( $this->tables[$t], $c ); continue; } if (!isset($c['primary'])) { $c['primary'] = null; } if (!isset($c['foreign'])) { $c['foreign'] = array(); } if (!is_array($c['foreign'])) { $c['foreign'] = array($c['foreign']); } $this->tables[$t] = $c; } return true; } /** * Get list of tables * * Get list of table names or complete table configuration * * @param bool $withConfig return list of tables with configuration * @return array */ public function getTables($withConfig = false) { if ($withConfig) { return $this->tables; } return array_keys($this->tables); } /** * Receive name of primary key, if any * * @param string $table * @param bool return identifier always as string * @return string|array|null column name(s) of primary key */ public function getIdentifier($table = null, $asString = false) { if (!$table) { return null; } $c = $this->getTableInfo($table); // force string if ($asString && is_array($c['primary'])) { return array_pop($c['primary']); } return $c['primary']; } /** * Inform event system * * Trigger event with prefix. In case event trigger is switched off, do nothing * * @param string $event, * @param string $table, * @param string $id * @param array $clause * @param array $save */ protected function trigger($event, $table, $id = null, $clause = array(), $save = array()) { if (!$this->doTriggerEvent) { return; } $eData = array( 'table' => $table, 'id' => $id, 'clause' => $clause, 'save' => $save ); WBEvent::trigger($this->eventPrefix . ':' . $event . ':' . $table, 'Saved record in table', $eData); } /** * Normalize Primary Key Settings * * @param array $info */ protected function normalizePrimary(&$info) { if (empty($info['primary'])) { return; } if (!is_array($info['primary'])) { $info['primary'] = array($info['primary']); } } /** * Get Id String * * Either simple primary id or multi column primary id in format COL0-COL1-COL2-... * * @param array table info * @param array data record * @return string */ protected function getIdString($info, $data) { if (empty($info['primary'])) { return ''; } if (!is_array($info['primary'])) { return $data[$info['primary']]; } $tmp = array(); foreach ($info['primary'] as $k) { $tmp[] = $data[$k]; } return implode('-', $tmp); } /** * Inject Id * * Primary Id may be concated with "-" to use multi column primary kays. * In case of multi column primary keys, use 0 for id parts that are not defined. * * @param string id string * @param array table info * @param array $clause */ protected function injectId2Clause($id, $info, &$clause) { if (empty($info['primary'])) { return; } $primary = $info['primary']; if (!is_array($primary)) { $primary = array($primary); } // reverse to get the least significant primay key part first $primary = array_reverse($primary); // last part of id is the "least significant" $tmp = explode('-', $id); foreach ($primary as $v) { // fallback is 0 $t = 0; if (!empty($tmp)) { $t = array_pop($tmp); } $clause[] = array( 'field' => $v, 'value' => $t ); } } /** * Add Column Option to Query * * @param string table name * @param array option list * @param array query list to build select string */ protected function addColumn($table, $options, &$query) { $c = $this->getTableInfo($table); $this->normalizePrimary($c); // only select a few columns $columns = array('*'); if( isset( $options['column'] ) ) { $columns = $options['column']; if (!is_array($columns)) { $columns = array($columns); } if (!empty($c['primary'])) { foreach ($c['primary'] as $p) { if (in_array( $c['primary'], $columns)) { continue; } $columns[] = $p; } } } foreach ($columns as &$col) { $col = $this->column2SelectString($table, $c, $col); } $distinct = $this->getDistinct($table, $options); if (!empty($distinct)) { // $columns[] = $distinct; $query[] = 'DISTINCT'; } $query[] = implode( ', ', $columns ); } /** * Column Parameter 2 Select String * * @see addColumn() * @param string table name * @param array table config * @param array column option * @return string */ private function column2SelectString($table, $c, $col) { if (!is_array($col)) { return $c['table'] . '.' . $col; } if (isset($col['field']) && is_array($col['field'])) { $res = array(); $tmp = $col; foreach ($col['field'] as $f) { $tmp['field'] = $f; $res[] = $this->column2SelectString($table, $c, $tmp); } return implode(', ', $res); } $cc = $c; if (isset($col['table']) && $col['table'] != $table) { $cc = $this->getTableInfo($col['table']); } $tmp = ''; if (isset($col['field'])) { $tmp = $cc['table'] . '.' . $col['field']; } if (isset($col['function'])) { $funcFmt = '%s(%s)'; if (!empty($col['format'])) { $funcFmt = $col['format']; } $tmp = sprintf($funcFmt, $col['function'], $tmp); } else if (isset($col['native'])) { $tmp = WBString::populate($col['native'], $c); } if (isset($col['as'])) { $tmp .= ' AS ' . $this->db->quote($col['as']); } return $tmp; } /** * Fetch table config * * This convenient function allows easy access to table config. * * @param string $table * @return array $config * @deprecated in favour of getTableInfo() */ protected function _getTableInfo( $table ) { return $this->getTableInfo($table); } /** * Fetch table config * * This convenient function allows easy access to table config. * * @param string $table * @return array $config */ public function getTableInfo($table) { if (!isset($this->tables[$table]['table'])) { WBClass::load('WBException_Config'); throw new WBException_Config('Could not find table "'. $table.'", maybe the config is corrupt.', 4, __CLASS__); } return $this->tables[$table]; } /** * Get list of all tables related to given one * * Collect all tables that have the table in their foreign list * * @param string $table * @return array */ public function getRelationTables($table) { if (!isset($this->tables[$table]['table'])) { WBClass::load('WBException_Config'); throw new WBException_Config('Could not find table "'. $table.'", maybe the config is corrupt.', 5, __CLASS__); } $rel = array(); foreach ($this->tables as $t => $info) { if (!isset($info['foreign']) || !is_array($info['foreign']) || empty($info['foreign'])) { continue; } if (!in_array($table, $info['foreign'])) { continue; } $rel[] = $t; } return $rel; } /** * Support for Most Common Joins * * Join table with right table. Optionally use different left table. One can * join with USING, ON with single column ON or multiple columns. Also LEFT, * and RIGHT joins are supported, as well as OUTER joins * * * @param string $table * @param array $options * @param array $query append join string to this query * @return bool always true */ protected function addJoin($table, $options, &$query) { if (!isset($options['join']) || !is_array($options['join'])) { return true; } foreach ($options['join'] as $join) { $other = $this->getTableInfo($join['table']); $left = ''; if (isset($join['left']) && !empty($join['left'])) { $left = 'LEFT '; if ('right' == $join['left']) { $left = 'RIGHT '; } } $tLeft = $this->getTableInfo($table); if (isset($join['lefttable']) && !empty($join['lefttable'])) { $tLeft = $this->getTableInfo($join['lefttable']); } if (isset($join['outer']) && !empty($join['outer'])) { $left .= 'OUTER '; } // most common case, example USING(c1) if (isset($join['using'])) { if ('__auto' == $join['using']) { $join['using'] = $this->getIdentifier($table); } if (is_array($join['using'])) { $join['using'] = implode(', ', $join['using']); } $query[] = sprintf('%sJOIN %s USING (%s)', $left, $other['table'], $join['using']); } // simple left equials right, single column, example ON c1=c2 else if (isset($join['onleft'])) { $query[] = sprintf('%sJOIN %s ON %s.%s=%s.%s', $left, $other['table'], $tLeft['table'], $join['onleft'], $other['table'], $join['onright']); } // multiple column join condition, example ON (c1=c2 AND c3=c4) else if (!empty($join['on'])) { $tmp = array(); foreach ($join['on'] as $on) { $tmp[] = sprintf('%s.%s=%s.%s', $tLeft['table'], $on['onleft'], $other['table'], $on['onright']); } $query[] = sprintf('%sJOIN %s ON (%s)', $left, $other['table'], implode(' AND ', $tmp)); } else { $alias = ''; if (!empty($join['alias'])) { $alias = ' ' . $join['alias']; } $query[] = sprintf('%sJOIN %s%s', $left, $other['table'], $alias); } } return true; } /** * Simple support for grouping resust sets * * * @param string $table * @param array $options * @param array $query append order string to this query * @return bool always true */ protected function addGroupBy($table, $options, &$query) { if (!isset($options['groupby']) || empty($options['groupby']) || !is_array($options['groupby'])) { return true; } if (!isset($options['groupby'][0])) { $options['groupby'] = array($options['groupby']); } $c = $this->getTableInfo($table); $string = array(); foreach ($options['groupby'] as $groupby) { $gTable = $c['table']; if (isset($groupby['table'])) { if (empty($groupby['table'])) { $gTable = ''; } else { $oc = $this->getTableInfo($groupby['table']); $gTable = $oc['table']; } } $having = ''; if (isset($groupby['having'])) { $having = 'HAVING ' . $groupby['having']; } if ('__auto' == $groupby['field']) { $groupby['field'] = $this->getIdentifier($table); } if (!is_array($groupby['field'])) { $groupby['field'] = array($groupby['field']); } foreach ($groupby['field'] as $f) { // allow to group by non column fields if (empty($gTable)) { $string[] = sprintf('%s %s', $f, $having); } else { $string[] = sprintf('%s.%s %s', $gTable, $f, $having); } } } $query[] = 'GROUP BY ' . implode(', ', $string); return true; } /** * create ORDER BY string * * Use options as well as table config to create ORDER BY string * * Order by just one column * * $options['order'] = array( 'field' => 'name' ); * * ...or by more colums * * $options['order'] = array( * array( 'field' => 'name' ), * array( 'field' => 'email' ), * array( 'table' => 'othertable', 'field' => 'orderpos' ), * array( 'field' => 'created', 'asc' => false ) * array( 'field' => 'name', 'asc' => true, 'function' => 'soundex' ) * ); * * * @param string $table * @param array $options * @param array $query append order string to this query * @return bool always true */ protected function _addOrderBy( $table, $options, &$query ) { $c = $this->getTableInfo( $table ); // get order options $order = array(); if( isset( $options['order'] ) && !empty( $options['order'] ) ) { $order = $options['order']; } else if( isset( $c['order'] ) ) { $order = $c['order']; } // nothing to do if( empty( $order ) ) { return true; } // force list of order arrays if( !isset( $order[0] ) ) { $order = array( $order ); } // build order string $string = array(); foreach( $order as $o ) { if( !isset( $o['field'] ) || empty( $o['field'] ) ) { continue; } $tmp = $c['table'] . '.' . $o['field']; if (isset($o['table']) && !empty($o['table'])) { $tmp = $this->tables[$o['table']]['table'] . '.' . $o['field']; } if (isset($o['function']) && !empty($o['function'])) { $tmp = $o['function'] . '(' . $tmp . ')'; } if (isset($o['native']) && !empty($o['native'])) { $tmp = $o['native']; } $dir = 'ASC'; if( isset( $o['asc'] ) && !$o['asc'] ) { $dir = 'DESC'; } $tmp .= ' ' . $dir; $string[] = $tmp; } $query[] = 'ORDER BY ' . implode( ', ', $string ); return true; } /** * create LIMIT string * * Use options as well as table config to create LIMIT string * * @param string $table * @param array $options * @param array $query append limit string to this query * @return bool always true */ protected function _addLimit( $table, $options, &$query ) { $c = $this->getTableInfo( $table ); $limit = self::MAX_LIMT; if( isset( $options['limit'] ) ) { if( $options['limit'] > 0 ) { $limit = $options['limit']; } } else if( isset( $c['limit'] ) ) { $limit = $c['limit']; } $offset = null; if( isset( $options['offset'] ) ) { $offset = null; if( $options['offset'] > 0 ) { $offset = $options['offset']; } } else if( isset( $c['offset'] ) ) { $offset = $c['offset']; } // really add limit? if( $limit == self::MAX_LIMT && $offset === null ) { return true; } if( $offset === null ) { $offset = 0; } $query[] = 'LIMIT ' . $offset . ',' . $limit; return true; } /** * Append foreign key relation to clause * * Helper function used for many selects * * @param string $table * @param string|null $foreign * @param array $clause * @return bool always true */ protected function _addForeignKey2Clause($table, $foreign, &$clause) { if ($foreign === null) { return true; } $f = $this->tables[$table]['foreign']; if (empty($f)) { // there is no proper foreign table defined return true; } // add first foreign key to clause $fKey = $this->tables[$f[0]]['primary']; $clause[] = array( 'field' => $fKey, 'value' => $foreign ); return true; } /** * Insert delete flag to clause * * Looks for "deleted" flag in config and automatically adds ist to clause. * Use options to switch off this feature * * @param string $table * @param array $clause * @param array $options * @return bool always true */ protected function _addDeletedFlag2Clause($table, &$clause, $options) { // see whether check for deleted flag if (isset($options['deleted']) && 0 < $options['deleted']) { return true; } $info = $this->getTableInfo($table); if (!isset($info['delete']) || empty($info['delete'])) { return true; } $clause[] = array( 'field' => trim($info['delete']), 'value' => 0 ); return true; } /** * Helper function translates clause-array to where array * * @deprecated in favour of clause2Where() * @param string $table named table as configured - which is just the default value * @param array $clause list of criterias * @param array $where list of where clause fragments * @return bool true on success, false otherwise */ protected function _clause2Where($table, $clause, &$where) { return $this->clause2Where($table, $clause, $where); } /** * Helper function translates clause-array to where array * * * Available keys of accotiative array: * - "field" tells which field the criteria affects * - "table" need to be specified in case you want to use a field of another table * - "value" the value for comparison * - "type" either "simple" (default), "native" or "complex". * - "bond" defines how to join complex clauses, this is either "and" or "or" (default) * - "relation" allows you to something different than "=" * - "valuetype" can be "function", "foreign" or "default" * * @param string $table named table as configured - which is just the default value * @param array $clause list of criterias * @param array $where list of where clause fragments * @return bool true on success, false otherwise */ public function clause2Where($table, $clause, &$where) { if (!is_array($clause) || empty($clause)) { return true; } // wrong argument! if (!isset($clause[0])) { WBClass::load('WBException_Argument'); throw new WBException_Argument('Clause must be an indexed array!', 6, __CLASS__); return false; } // manage clause for( $i = 0; $i < count( $clause ); ++$i ) { if( !isset( $clause[$i]['table'] ) ) { $clause[$i]['table'] = $table; } $type = 'simple'; if( isset( $clause[$i]['type'] ) ) { $type = $clause[$i]['type']; } $whereClause = ''; switch( strtolower( $type ) ) { case 'complex': $myWhere = array(); $this->clause2Where( $table, $clause[$i]['clause'], $myWhere ); $bond = 'OR'; if( isset( $clause[$i]['bond'] ) ) { $bond = strtoupper( $clause[$i]['bond'] ); } $whereClause = '( ' . implode( ' ' . $bond . ' ', $myWhere ) . ' )'; break; case 'native': if( !isset( $clause[$i]['clause'] ) || empty( $clause[$i]['clause'] ) ) { break; } if( !isset( $clause[$i]['values'] ) || empty( $clause[$i]['values'] ) ) { $whereClause = '(' . $clause[$i]['clause'] . ')'; break; } $values = array( $clause[$i]['values'] ); if( !is_array( $values ) ) { $values = array( $values ); } foreach( $clause[$i]['values'] as $value ) { $values[] = $this->db->quote( $value ); } $whereClause = '(' . vsprintf( $clause[$i]['clause'], $values ) . ')'; break; case 'simple'; default: $whereClause = $this->getWhereClause($clause[$i]); break; } if (!empty($whereClause)) { $where[] = $whereClause; } } return true; } /** * Get Where Clause * * @param array * @deprecated in favour of getWhereClause() */ protected function _getWhereClause( $clause ) { return $this->getWhereClause($clause); } /** * build where string * * Supported relations: * - "equal" (default) * - "begin" * - "like" * - "in_set" * - "in" * - "native" * - "gt" greater than * - "ge" greater or equal * - "not" * - "not_in_set" * - "le" lower or equal * - "lt" lower than * * @param array $clause * @return string partial clause * @see clause2Where */ protected function getWhereClause( $clause ) { // array if (is_array($clause['field'])) { $where = array(); $value = explode('-', $clause['value']); $c = $clause; for ($i = (count($clause['field']) - 1); $i >= 0; --$i) { $v = 0; if (!empty($value)) { $v = array_pop($value); } $c['field'] = $clause['field'][$i]; $c['value'] = $v; $where[] = $this->getWhereClause($c); } return implode(' AND ', $where); } if (!isset($clause['valuetype'])) { $clause['valuetype'] = '_default_'; } $quote = false; switch (strtolower($clause['valuetype'])) { case 'function': $value = $clause['value']; break; case 'foreign': $foreign = $this->tables[$clause['foreign']]['table']; $value = $foreign . '.' . $clause['value']; break; case 'native': $value = $clause['value']; break; default: $value = $clause['value']; $quote = true; break; } if (!isset($clause['relation'])) { $clause['relation'] = '_default_'; } // select relation switch (strtolower($clause['relation'])) { case 'native': $rel = '%s ' . $clause['operation'] . ' %s'; break; case 'begin': case 'begins': $rel = "%s LIKE %s"; $value = "'" . $this->db->quote($value, true) . "%'"; $quote = false; break; case 'not_begin': case 'not_begins': $rel = "%s NOT LIKE %s"; $value = "'" . $this->db->quote($value, true) . "%'"; $quote = false; break; case 'like': $rel = "%s LIKE %s"; $value = "'%" . $this->db->quote($value, true) . "%'"; $quote = false; break; case 'not_like': $rel = "%s NOT LIKE %s"; $value = "'%" . $this->db->quote($value, true) . "%'"; $quote = false; break; case 'in_set': $rel = 'FIND_IN_SET(%2$s, %1$s)'; break; case 'not_in_set': $rel = 'NOT FIND_IN_SET(%2$s, %1$s)'; break; case 'in_or_out': if (!is_array($value)) { $value = array_map('trim', explode(',', $value)); } $in = array(); $out = array(); foreach ($value as $v) { if ('!' == $v[0]) { $out[] = substr($v, 1); } else { $in[] = $v; } } $tmp = array(); if (!empty($in)) { $c = array( 'relation' => 'in', 'table' => $clause['table'], 'field' => $clause['field'], 'value' => $in ); $tmp[] = $this->getWhereClause($c); } if (!empty($out)) { $c = array( 'relation' => 'not_in', 'table' => $clause['table'], 'field' => $clause['field'], 'value' => $out ); $tmp[] = $this->getWhereClause($c); } return implode(' AND ', $tmp); break; case 'not_in': case 'in': $rel = '%s IN (%s)'; if (strtolower($clause['relation']) == 'not_in') { $rel = '%s NOT IN (%s)'; } if (!is_array($value)) { $value = array($value); } $tmp = array(); foreach ($value as $v) { $tmp[] = $this->db->quote($v); } $value = implode(', ', $tmp); $quote = false; break; case 'lt': case '<': $rel = '%s<%s'; break; case 'le': case '<=': $rel = '%s<=%s'; break; case 'gt': case '>': $rel = '%s>%s'; break; case 'ge': case '>=': $rel = '%s>=%s'; break; case 'ne': case '<>': case 'not': $rel = '%s<>%s'; break; case 'null': $rel = '%s IS %s NULL'; $quote = false; if ($value) { $value = ''; } else { $value = 'NOT'; } break; case 'eq': case 'equal': default: $rel = '%s=%s'; if (isset($clause['cast']) && !empty($clause['cast'])) { $value = sprintf('cast(%s AS %s)', $this->db->quote($value), $clause['cast']); $quote = false; } break; } if ($quote) { $value = $this->db->quote($value); } return sprintf($rel, $this->tables[$clause['table']]['table'] . '.' . $clause['field'], $value); } /** * Get Distinct Snippet * * @param string primary table name * @param array option list * @return string */ protected function getDistinct($table, $option) { if (empty($option['distinct'])) { return ''; } if (!is_array($option['distinct'])) { $option['distinct'] = array($option['distinct']); } $distinct = array(); foreach ($option['distinct'] as $d) { if (!is_array($d)) { $d = array('field' => $d); } if (empty($d['table'])) { $d['table'] = $table; } $c = $this->getTableInfo($d['table']); $distinct[] = sprintf('%s.%s', $c['table'], $d['field']); } return sprintf('DISTINCT %s', implode(', ', $distinct)); } }