query()} method (and it's 'select', etc short-cuts). * Typically it would not be initialised directly. * * Note that this is a stub class that a driver will extend and complete as * required for individual database types. Individual drivers could add * additional methods, but this is discouraged to ensure that the API is the * same for all database types. */ abstract class Query { /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Constructor */ /** * Query instance constructor. * * Note that typically instances of this class will be automatically created * through the {@see \DataTables\Database->query()} method. * @param Database $db Database instance * @param string $type Query type - 'select', 'insert', 'update' or 'delete' * @param string|string[] $table Tables to operate on - see {@see Query->table()}. */ public function __construct( $dbHost, $type, $table=null ) { $this->_dbHost = $dbHost; $this->_type = $type; $this->table( $table ); } /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Private properties */ /** * @var Database Database connection instance * @internal */ protected $_dbHost; /** * @var string Driver to use * @internal */ protected $_type = ""; /** * @var array * @internal */ protected $_table = array(); /** * @var array * @internal */ protected $_field = array(); /** * @var array * @internal */ protected $_bindings = array(); /** * @var array * @internal */ protected $_where = array(); /** * @var array * @internal */ protected $_join = array(); /** * @var array * @internal */ protected $_order = array(); /** * @var array * @internal */ protected $_noBind = array(); /** * @var int * @internal */ protected $_limit = null; /** * @var int * @internal */ protected $_group_by = null; /** * @var int * @internal */ protected $_offset = null; /** * @var string * @internal */ protected $_distinct = false; /** * @var string * @internal */ protected $_identifier_limiter = array( '`', '`' ); /** * @var string * @internal */ protected $_field_quote = '\''; /** * @var array * @internal */ protected $_pkey = null; protected $_supportsAsAlias = true; protected $_whereInCnt = 1; /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Static methods */ /** * Commit a transaction. * @param \PDO $dbh The Database handle (typically a PDO object, but not always). */ public static function commit ( $dbh ) { $dbh->commit(); } /** * Database connection - override by the database driver. * @param string|array $user User name or all parameters in an array * @param string $pass Password * @param string $host Host name * @param string $db Database name * @return Query */ public static function connect ( $user, $pass='', $host='', $port='', $db='', $dsn='' ) { // noop - PHP <7 can't have an abstract static without causing // an error in strict mode. This should technically be an // abstract method however. } /** * Start a database transaction * @param \PDO $dbh The Database handle (typically a PDO object, but not always). */ public static function transaction ( $dbh ) { $dbh->beginTransaction(); } /** * Rollback the database state to the start of the transaction. * @param \PDO $dbh The Database handle (typically a PDO object, but not always). */ public static function rollback ( $dbh ) { $dbh->rollBack(); } /** * Common helper for the drivers to handle a PDO DSN postfix * @param string $dsn DSN postfix to use * @return Query * @internal */ static function dsnPostfix ( $dsn ) { if ( ! $dsn ) { return ''; } // Add a DSN field separator if not given if ( strpos( $dsn, ';' ) !== 0 ) { return ';'.$dsn; } return $dsn; } /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Public methods */ /** * Safely bind an input value to a parameter. This is evaluated when the * query is executed. This allows user input to be safely executed without * risk of an SQL injection attack. * * @param string $name Parameter name. This should include a leading colon * @param string $value Value to bind * @param mixed $type Data type. See the PHP PDO documentation: * http://php.net/manual/en/pdo.constants.php * @return Query */ public function bind ( $name, $value, $type=null ) { $this->_bindings[] = array( "name" => $this->_safe_bind( $name ), "value" => $value, "type" => $type ); return $this; } /** * Get the Database host for this query instance * @return DataTable Database class instance */ public function database () { return $this->_dbHost; } /** * Set a distinct flag for a `select` query. Note that this has no effect on * any of the other query types. * @param boolean $dis Optional * @return Query */ public function distinct ( $dis ) { $this->_distinct = $dis; return $this; } /** * Execute the query. * @param string $sql SQL string to execute (only if _type is 'raw'). * @return Result */ public function exec ( $sql=null ) { $type = strtolower( $this->_type ); if ( $type === 'select' ) { return $this->_select(); } else if ( $type === 'insert' ) { return $this->_insert(); } else if ( $type === 'update' ) { return $this->_update(); } else if ( $type === 'delete' ) { return $this->_delete(); } else if ( $type === 'count' ) { return $this->_count(); } else if ( $type === 'raw' ) { return $this->_raw( $sql ); } throw new \Exception("Unknown database command or not supported: ".$type, 1); } /** * Get fields. * @param string|string[] $get,... Fields to get - can be specified as * individual fields or an array of fields. * @return self */ public function get ( $get ) { $args = func_get_args(); if ( $get === null ) { return $this; } for ( $i=0 ; $iget( $args[$i][$j] ); } } else if ( strpos($args[$i], ',') !== false && strpos($args[$i], '(') === false) { // Comma delimited set of fields - legacy. Recommended that fields be split into // an array on input $a = explode(',', $args[$i]); for ( $j=0 ; $jget( $a[$j] ); } } else { $this->_field[] = trim( $args[$i] ); } } return $this; } /** * Perform a JOIN operation * @param string $table Table name to do the JOIN on * @param string $condition JOIN condition * @param string $type JOIN type * @return self */ public function join ( $table, $condition, $type='', $bind=true ) { // Tidy and check we know what the join type is if ($type !== '') { $type = strtoupper(trim($type)); if ( ! in_array($type, array('LEFT', 'RIGHT', 'INNER', 'OUTER', 'LEFT OUTER', 'RIGHT OUTER'))) { $type = ''; } } // Protect the identifiers if ($bind && preg_match('/([\w\.]+)([\W\s]+)(.+)/', $condition, $match)) { $match[1] = $this->_protect_identifiers( $match[1] ); $match[3] = $this->_protect_identifiers( $match[3] ); $condition = $match[1].$match[2].$match[3]; } $this->_join[] = $type .' JOIN '. $this->_protect_identifiers($table) .' ON '. $condition .' '; return $this; } /** * Add a left join, with common logic for handling binding or not */ public function left_join( $joins ) { // Allow a single associative array if ($this->_is_assoc($joins)) { $joins = array( $joins ); } for ( $i=0, $ien=count($joins) ; $i<$ien ; $i++ ) { $join = $joins[$i]; if ($join['field2'] === null && $join['operator'] === null) { $this->join( $join['table'], $join['field1'], 'LEFT', false ); } else { $this->join( $join['table'], $join['field1'].' '.$join['operator'].' '.$join['field2'], 'LEFT' ); } } return $this; } /** * Limit the result set to a certain size. * @param int $lim The number of records to limit the result to. * @return self */ public function limit ( $lim ) { $this->_limit = $lim; return $this; } /** * Group the results by the values in a field * @param string The field of which the values are to be grouped * @return self */ public function group_by ( $group_by ) { $this->_group_by = $group_by; return $this; } /** * Get / set the primary key column name(s) so they can be easily returned * after an insert. * @param string[] $pkey Primary keys * @return Query|string[] */ public function pkey ( $pkey=null ) { if ( $pkey === null ) { return $this->_pkey; } $this->_pkey = $pkey; return $this; } /** * Set table(s) to perform the query on. * @param string|string[] $table,... Table(s) to use - can be specified as * individual names, an array of names, a string of comma separated * names or any combination of those. * @return self */ public function table ( $table ) { if ( $table === null ) { return $this; } if ( is_array($table) ) { // Array so loop internally for ( $i=0 ; $itable( $table[$i] ); } } else { // String based, explode for multiple tables $tables = explode(",", $table); for ( $i=0 ; $i_table[] = $this->_protect_identifiers( trim($tables[$i]) ); } } return $this; } /** * Offset the return set by a given number of records (useful for paging). * @param int $off The number of records to offset the result by. * @return self */ public function offset ( $off ) { $this->_offset = $off; return $this; } /** * Order by * @param string|string[] $order Columns and direction to order by - can * be specified as individual names, an array of names, a string of comma * separated names or any combination of those. * @return self */ public function order ( $order ) { if ( $order === null ) { return $this; } if ( !is_array($order) ) { $order = preg_split('/\,(?![^\(]*\))/',$order); } for ( $i=0 ; $i_order[] = $this->_protect_identifiers( $identifier ).' '.$direction; } return $this; } /** * Set fields to a given value. * * Can be used in two different ways, as set( field, value ) or as an array of * fields to set: set( array( 'fieldName' => 'value', ...) ); * @param string|string[] $set Can be given as a single string, when then $val * must be set, or as an array of key/value pairs to be set. * @param string $val When $set is given as a simple string, $set is the field * name and this is the field's value. * @param boolean $bind Should the value be bound or not * @return self */ public function set ( $set, $val=null, $bind=true ) { if ( $set === null ) { return $this; } if ( !is_array($set) ) { $set = array( $set => $val ); } foreach ($set as $key => $value) { $this->_field[] = $key; if ( $bind ) { $this->bind( ':'.$key, $value ); } else { $this->_noBind[$key] = $value; } } return $this; } /** * Where query - multiple conditions are bound as ANDs. * * Can be used in two different ways, as where( field, value ) or as an array of * conditions to use: where( array('fieldName', ...), array('value', ...) ); * @param string|string[]|callable $key Single field name, or an array of field names. * If given as a function (i.e. a closure), the function is called, passing the * query itself in as the only parameter, so the function can add extra conditions * with parentheses around the additional parameters. * @param string|string[] $value Single field value, or an array of * values. Can be null to search for `IS NULL` or `IS NOT NULL` (depending * on the value of `$op` which should be `=` or `!=`. * @param string $op Condition operator: <, >, = etc * @param boolean $bind Escape the value (true, default) or not (false). * @return self * * @example * The following will produce * `'WHERE name='allan' AND ( location='Scotland' OR location='Canada' )`: * * ```php * $query * ->where( 'name', 'allan' ) * ->where( function ($q) { * $q->where( 'location', 'Scotland' ); * $q->or_where( 'location', 'Canada' ); * } ); * ``` */ public function where ( $key, $value=null, $op="=", $bind=true ) { if ( $key === null ) { return $this; } else if ( is_callable($key) && is_object($key) ) { // is a closure $this->_where_group( true, 'AND' ); $key( $this ); $this->_where_group( false, 'OR' ); } else if ( !is_array($key) && is_array($value) ) { for ( $i=0 ; $iwhere( $key, $value[$i], $op, $bind ); } } else { $this->_where( $key, $value, 'AND ', $op, $bind ); } return $this; } /** * Add addition where conditions to the query with an AND operator. An alias * of `where` for naming consistency. * * Can be used in two different ways, as where( field, value ) or as an array of * conditions to use: where( array('fieldName', ...), array('value', ...) ); * @param string|string[]|callable $key Single field name, or an array of field names. * If given as a function (i.e. a closure), the function is called, passing the * query itself in as the only parameter, so the function can add extra conditions * with parentheses around the additional parameters. * @param string|string[] $value Single field value, or an array of * values. Can be null to search for `IS NULL` or `IS NOT NULL` (depending * on the value of `$op` which should be `=` or `!=`. * @param string $op Condition operator: <, >, = etc * @param boolean $bind Escape the value (true, default) or not (false). * @return self */ public function and_where ( $key, $value=null, $op="=", $bind=true ) { return $this->where( $key, $value, $op, $bind ); } /** * Add addition where conditions to the query with an OR operator. * * Can be used in two different ways, as where( field, value ) or as an array of * conditions to use: where( array('fieldName', ...), array('value', ...) ); * @param string|string[]|callable $key Single field name, or an array of field names. * If given as a function (i.e. a closure), the function is called, passing the * query itself in as the only parameter, so the function can add extra conditions * with parentheses around the additional parameters. * @param string|string[] $value Single field value, or an array of * values. Can be null to search for `IS NULL` or `IS NOT NULL` (depending * on the value of `$op` which should be `=` or `!=`. * @param string $op Condition operator: <, >, = etc * @param boolean $bind Escape the value (true, default) or not (false). * @return self */ public function or_where ( $key, $value=null, $op="=", $bind=true ) { if ( $key === null ) { return $this; } else if ( is_callable($key) && is_object($key) ) { $this->_where_group( true, 'OR' ); $key( $this ); $this->_where_group( false, 'OR' ); } else { if ( !is_array($key) && is_array($value) ) { for ( $i=0 ; $ior_where( $key, $value[$i], $op, $bind ); } return $this; } $this->_where( $key, $value, 'OR ', $op, $bind ); } return $this; } /** * Provide grouping for WHERE conditions. Use it with a callback function to * automatically group any conditions applied inside the method. * * For legacy reasons this method also provides the ability to explicitly * define if a grouping bracket should be opened or closed in the query. * This method is not prefer. * * @param boolean|callable $inOut If callable it will create the group * automatically and pass the query into the called function. For * legacy operations use `true` to open brackets, `false` to close. * @param string $op Conditional operator to use to join to the * preceding condition. Default `AND`. * @return self * * @example * ```php * $query->where_group( function ($q) { * $q->where( 'location', 'Edinburgh' ); * $q->where( 'position', 'Manager' ); * } ); * ``` */ public function where_group ( $inOut, $op='AND' ) { if ( is_callable($inOut) && is_object($inOut) ) { $this->_where_group( true, $op ); $inOut( $this ); $this->_where_group( false, $op ); } else { $this->_where_group( $inOut, $op ); } return $this; } /** * Provide a method that can be used to perform a `WHERE ... IN (...)` query * with bound values and parameters. * * Note this is only suitable for local values, not a sub-query. For that use * `->where()` with an unbound value. * * @param string Field name * @param array Values * @param string Conditional operator to use to join to the * preceding condition. Default `AND`. * @return self */ public function where_in ( $field, $arr, $operator="AND" ) { if ( count($arr) === 0 ) { return $this; } $binders = array(); $prefix = ':wherein'; // Need to build an array of the binders (having bound the values) so // the query can be constructed for ( $i=0, $ien=count($arr) ; $i<$ien ; $i++ ) { $binder = $prefix.$this->_whereInCnt; $this->bind( $binder, $arr[$i] ); $binders[] = $binder; $this->_whereInCnt++; } $this->_where[] = array( 'operator' => $operator, 'group' => null, 'field' => $this->_protect_identifiers($field), 'query' => $this->_protect_identifiers($field) .' IN ('.implode(', ', $binders).')' ); return $this; } /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Protected methods */ /** * Create a comma separated field list * @param bool $addAlias Flag to add an alias * @return string * @internal */ protected function _build_field( $addAlias=false ) { $a = array(); $asAlias = $this->_supportsAsAlias ? ' as ' : ' '; for ( $i=0 ; $i_field) ; $i++ ) { $field = $this->_field[$i]; // Keep the name when referring to a table if ( $addAlias && $field !== '*' && strpos($field, '(') === false ) { $split = preg_split( '/ as (?![^\(]*\))/i', $field ); if ( count($split) > 1 ) { $a[] = $this->_protect_identifiers( $split[0] ).$asAlias. $this->_field_quote. $split[1] .$this->_field_quote; } else { $a[] = $this->_protect_identifiers( $field ).$asAlias. $this->_field_quote. $this->_escape_field($field) .$this->_field_quote; } } else if ( $addAlias && strpos($field, '(') !== false && ! strpos($field, ' as ') ) { $a[] = $this->_protect_identifiers( $field ).$asAlias. $this->_field_quote. $this->_escape_field($field) .$this->_field_quote; } else { $a[] = $this->_protect_identifiers( $field ); } } return ' '.implode(', ', $a).' '; } /** * Create a JOIN statement list * @return string * @internal */ protected function _build_join() { return implode(' ', $this->_join); } /** * Create the LIMIT / OFFSET string * * MySQL and Postgres style - anything else can have the driver override * @return string * @internal */ protected function _build_limit() { $out = ''; $limit = intval($this->_limit); $offset = intval($this->_offset); if ( $limit ) { $out .= ' LIMIT '.$limit; } if ( $offset ) { $out .= ' OFFSET '.$offset; } return $out; } /** * Create the GROUP BY string * * @return string * @internal */ protected function _build_group_by() { $out = ''; if ( $this->_group_by) { $out .= ' GROUP BY '.$this->_protect_identifiers( $this->_group_by ); } return $out; } /** * Create the ORDER BY string * @return string * @internal */ protected function _build_order() { if ( count( $this->_order ) > 0 ) { return ' ORDER BY '.implode(', ', $this->_order).' '; } return ''; } /** * Create a set list * @return string * @internal */ protected function _build_set() { $a = array(); for ( $i=0 ; $i_field) ; $i++ ) { $field = $this->_field[$i]; if ( isset( $this->_noBind[ $field ] ) ) { $a[] = $this->_protect_identifiers( $field ) .' = '. $this->_noBind[ $field ]; } else { $a[] = $this->_protect_identifiers( $field ) .' = :'. $this->_safe_bind( $field ); } } return ' '.implode(', ', $a).' '; } /** * Create the TABLE list * @return string * @internal */ protected function _build_table() { if ( $this->_type === 'insert' ) { // insert, update and delete statements don't need or want aliases in the table name $a = array(); for ( $i=0, $ien=count($this->_table) ; $i<$ien ; $i++ ) { $table = str_ireplace( ' as ', ' ', $this->_table[$i] ); $tableParts = explode( ' ', $table ); $a[] = $tableParts[0]; } return ' '.implode(', ', $a).' '; } return ' '.implode(', ', $this->_table).' '; } /** * Create a bind field value list * @return string * @internal */ protected function _build_value() { $a = array(); for ( $i=0, $ien=count($this->_field) ; $i<$ien ; $i++ ) { $a[] = ' :'.$this->_safe_bind( $this->_field[$i] ); } return ' '.implode(', ', $a).' '; } /** * Create the WHERE statement * @return string * @internal */ protected function _build_where() { if ( count($this->_where) === 0 ) { return ""; } $condition = "WHERE "; for ( $i=0 ; $i_where) ; $i++ ) { if ( $i === 0 ) { // Nothing (simplifies the logic!) } else if ( $this->_where[$i]['group'] === ')' ) { // If a group has been used but no conditions were added inside // of, we don't want to end up with `()` in the SQL as that is // invalid, so add a 1. if ( $this->_where[$i-1]['group'] === '(' ) { $condition .= '1=1'; } // else nothing } else if ( $this->_where[$i-1]['group'] === '(' ) { // Nothing } else { $condition .= $this->_where[$i]['operator'].' '; } if ( $this->_where[$i]['group'] !== null ) { $condition .= $this->_where[$i]['group']; } else { $condition .= $this->_where[$i]['query'] .' '; } } return $condition; } /** * Create a DELETE statement * @return Result * @internal */ protected function _delete() { $this->_prepare( 'DELETE FROM ' .$this->_build_table() .$this->_build_where() ); return $this->_exec(); } /** * Escape quotes in a field identifier * @internal */ protected function _escape_field( $field ) { $quote = $this->_field_quote; return str_replace($quote, "\\".$quote, $field); } /** * Execute the query. Provided by the driver * @return Result * @internal */ abstract protected function _exec(); /** * Create an INSERT statement * @return Result * @internal */ protected function _insert() { $this->_prepare( 'INSERT INTO ' .$this->_build_table().' (' .$this->_build_field() .') ' .'VALUES (' .$this->_build_value() .')' ); return $this->_exec(); } /** * Prepare the SQL query by populating the bound variables. * Provided by the driver * @return void * @internal */ abstract protected function _prepare( $sql ); /** * Protect field names * @param string $identifier String to be protected * @return string * @internal */ protected function _protect_identifiers( $identifier ) { $idl = $this->_identifier_limiter; // No escaping character if ( ! $idl ) { return $identifier; } $left = $idl[0]; $right = $idl[1]; // Dealing with a function or other expression? Just return immediately if (strpos($identifier, '(') !== FALSE || strpos($identifier, '*') !== FALSE) { return $identifier; } // Going to be operating on the spaces in strings, to simplify the white-space $identifier = preg_replace('/[\t ]+/', ' ', $identifier); $identifier = str_replace(' as ', ' ', $identifier); // If more that a single space, then return if (substr_count($identifier, ' ') > 1) { return $identifier; } // Find if our identifier has an alias, so we don't escape that if ( strpos($identifier, ' ') !== false ) { $alias = strstr($identifier, ' '); $identifier = substr($identifier, 0, - strlen($alias)); } else { $alias = ''; } $a = explode('.', $identifier); return $left . implode($right.'.'.$left, $a) . $right . $alias; } /** * Passed in SQL statement * @return Result * @internal */ protected function _raw( $sql ) { $this->_prepare( $sql ); return $this->_exec(); } /** * The characters that can be used for the PDO bindValue name are quite * limited (`[a-zA-Z0-9_]+`). We need to abstract this out to allow slightly * more complex expressions including dots for easy aliasing * @param string $name Field name * @return string * @internal */ protected function _safe_bind ( $name ) { $name = str_replace('.', '_1_', $name); $name = str_replace('-', '_2_', $name); $name = str_replace('/', '_3_', $name); $name = str_replace('\\', '_4_', $name); $name = str_replace(' ', '_5_', $name); return $name; } /** * Create a SELECT statement * @return Result * @internal */ protected function _select() { $this->_prepare( 'SELECT '.($this->_distinct ? 'DISTINCT ' : '') .$this->_build_field( true ) .'FROM '.$this->_build_table() .$this->_build_join() .$this->_build_where() .$this->_build_group_by() .$this->_build_order() .$this->_build_limit() ); return $this->_exec(); } /** * Create a SELECT COUNT statement * @return Result * @internal */ protected function _count() { $select = $this->_supportsAsAlias ? 'SELECT COUNT('.$this->_build_field().') as '.$this->_protect_identifiers('cnt') : 'SELECT COUNT('.$this->_build_field().') '.$this->_protect_identifiers('cnt'); $this->_prepare( $select .' FROM '.$this->_build_table() .$this->_build_join() .$this->_build_where() .$this->_build_limit() ); return $this->_exec(); } /** * Create an UPDATE statement * @return Result * @internal */ protected function _update() { $this->_prepare( 'UPDATE ' .$this->_build_table() .'SET '.$this->_build_set() .$this->_build_where() ); return $this->_exec(); } /** * Add an individual where condition to the query. * @internal * @param $where * @param null $value * @param string $type * @param string $op * @param bool $bind */ protected function _where ( $where, $value=null, $type='AND ', $op="=", $bind=true ) { if ( $where === null ) { return; } else if ( !is_array($where) ) { $where = array( $where => $value ); } foreach ($where as $key => $value) { $i = count( $this->_where ); if ( $value === null ) { // Null query $this->_where[] = array( 'operator' => $type, 'group' => null, 'field' => $this->_protect_identifiers($key), 'query' => $this->_protect_identifiers($key) .( $op === '=' ? ' IS NULL' : ' IS NOT NULL') ); } else if ( $bind ) { // Binding condition (i.e. escape data) if ( $this->_dbHost->type() === 'Postgres' && $op === 'like' ) { // Postgres specific a it needs a case for string searching non-text data $this->_where[] = array( 'operator' => $type, 'group' => null, 'field' => $this->_protect_identifiers($key), 'query' => $this->_protect_identifiers($key).'::text ilike '.$this->_safe_bind(':where_'.$i) ); } else { $this->_where[] = array( 'operator' => $type, 'group' => null, 'field' => $this->_protect_identifiers($key), 'query' => $this->_protect_identifiers($key) .' '.$op.' '.$this->_safe_bind(':where_'.$i) ); } $this->bind( ':where_'.$i, $value ); } else { // Non-binding condition $this->_where[] = array( 'operator' => $type, 'group' => null, 'field' => null, 'query' => $this->_protect_identifiers($key) .' '. $op .' '. $this->_protect_identifiers($value) ); } } } /** * Add parentheses to a where condition * @return string * @internal */ protected function _where_group ( $inOut, $op ) { $this->_where[] = array( "group" => $inOut ? '(' : ')', "operator" => $op ); } /** * Check if an array is associative or sequential */ private function _is_assoc(array $arr) { if (array() === $arr) { return false; } return array_keys($arr) !== range(0, count($arr) - 1); } };