diff --git a/dibi/dibi.php b/dibi/dibi.php index 5b6cd859..2558721f 100644 --- a/dibi/dibi.php +++ b/dibi/dibi.php @@ -52,6 +52,7 @@ require_once dirname(__FILE__) . '/libs/DibiTranslator.php'; require_once dirname(__FILE__) . '/libs/DibiVariable.php'; require_once dirname(__FILE__) . '/libs/DibiTable.php'; require_once dirname(__FILE__) . '/libs/DibiDataSource.php'; +require_once dirname(__FILE__) . '/libs/DibiFluent.php'; @@ -424,11 +425,76 @@ class dibi */ protected static function __callStatic($name, $args) { + //if ($name = 'select', 'update', ...') { + // return self::command()->$name($args); + //} return call_user_func_array(array(self::getConnection(), $name), $args); } + /********************* fluent SQL builders ****************d*g**/ + + + + /** + * @return DibiFluent + */ + public static function command() + { + return new DibiFluent(self::getConnection()); + } + + + + /** + * @param string column name + * @return DibiFluent + */ + public static function select($args) + { + $args = func_get_args(); + return self::command()->__call('select', $args); + } + + + + /** + * @param string table + * @param array + * @return DibiFluent + */ + public static function update($table, array $args) + { + return self::command()->update('%n', $table)->set($args); + } + + + + /** + * @param string table + * @param array + * @return DibiFluent + */ + public static function insert($table, array $args) + { + return self::command()->insert() + ->into('%n', $table, '(%n)', array_keys($args))->values('%l', array_values($args)); + } + + + + /** + * @param string table + * @return DibiFluent + */ + public static function delete($table) + { + return self::command()->delete()->from('%n', $table); + } + + + /********************* data types ****************d*g**/ @@ -593,8 +659,8 @@ class dibi } else { if ($sql === NULL) $sql = self::$sql; + static $keywords1 = 'SELECT|UPDATE|INSERT(?:\s+INTO)?|REPLACE(?:\s+INTO)?|DELETE|FROM|WHERE|HAVING|GROUP\s+BY|ORDER\s+BY|LIMIT|SET|VALUES|LEFT\s+JOIN|INNER\s+JOIN|TRUNCATE'; static $keywords2 = 'ALL|DISTINCT|DISTINCTROW|AS|USING|ON|AND|OR|IN|IS|NOT|NULL|LIKE|TRUE|FALSE'; - static $keywords1 = 'SELECT|UPDATE|INSERT(?:\s+INTO)|REPLACE(?:\s+INTO)|DELETE|FROM|WHERE|HAVING|GROUP\s+BY|ORDER\s+BY|LIMIT|SET|VALUES|LEFT\s+JOIN|INNER\s+JOIN'; // insert new lines $sql = ' ' . $sql; @@ -647,7 +713,8 @@ class dibi { return array( 'dibi version: ' . dibi::VERSION, - 'Number or queries: ' . dibi::$numOfQueries . (dibi::$totalTime === NULL ? '' : ' (elapsed time: ' . sprintf('%0.3f', dibi::$totalTime * 1000) . ' ms)'), + 'Number or queries: ' . dibi::$numOfQueries + . (dibi::$totalTime === NULL ? '' : ' (elapsed time: ' . sprintf('%0.3f', dibi::$totalTime * 1000) . ' ms)'), ); } diff --git a/dibi/drivers/mysql.php b/dibi/drivers/mysql.php index 9e82b109..0e451cc6 100644 --- a/dibi/drivers/mysql.php +++ b/dibi/drivers/mysql.php @@ -272,7 +272,7 @@ class DibiMySqlDriver extends /*Nette::*/Object implements IDibiDriver // see http://dev.mysql.com/doc/refman/5.0/en/select.html $sql .= ' LIMIT ' . ($limit < 0 ? '18446744073709551615' : (int) $limit) - . ($offset > 0 ? ' OFFSET ' . (int) $offset : ''); + . ($offset > 0 ? ' OFFSET ' . (int) $offset : ''); } diff --git a/dibi/drivers/mysqli.php b/dibi/drivers/mysqli.php index bfbdd663..44ad665f 100644 --- a/dibi/drivers/mysqli.php +++ b/dibi/drivers/mysqli.php @@ -252,7 +252,7 @@ class DibiMySqliDriver extends /*Nette::*/Object implements IDibiDriver // see http://dev.mysql.com/doc/refman/5.0/en/select.html $sql .= ' LIMIT ' . ($limit < 0 ? '18446744073709551615' : (int) $limit) - . ($offset > 0 ? ' OFFSET ' . (int) $offset : ''); + . ($offset > 0 ? ' OFFSET ' . (int) $offset : ''); } diff --git a/dibi/libs/DibiFluent.php b/dibi/libs/DibiFluent.php new file mode 100644 index 00000000..a04a8aea --- /dev/null +++ b/dibi/libs/DibiFluent.php @@ -0,0 +1,312 @@ + array('SELECT', 'DISTINCT', 'FROM', 'WHERE', 'GROUP BY', + 'HAVING', 'ORDER BY', 'LIMIT', 'OFFSET', '%end'), + 'UPDATE' => array('UPDATE', 'SET', 'WHERE', 'ORDER BY', 'LIMIT', '%end'), + 'INSERT' => array('INSERT', 'INTO', 'VALUES', 'SELECT', '%end'), + 'DELETE' => array('DELETE', 'FROM', 'USING', 'WHERE', 'ORDER BY', 'LIMIT', '%end'), + ); + + /** @var array */ + public static $separators = array( + 'SELECT' => ',', + 'FROM' => FALSE, + 'WHERE' => 'AND', + 'GROUP BY' => ',', + 'HAVING' => 'AND', + 'ORDER BY' => ',', + 'LIMIT' => FALSE, + 'OFFSET' => FALSE, + 'SET' => ',', + 'VALUES' => ',', + 'INTO' => FALSE, + ); + + /** @var DibiConnection */ + private $connection; + + /** @var string */ + private $command; + + /** @var array */ + private $clauses = array(); + + /** @var array */ + private $flags = array(); + + /** @var array */ + private $cursor; + + + + /** + * @param DibiConnection + */ + public function __construct(DibiConnection $connection) + { + $this->connection = $connection; + } + + + + /** + * Appends new argument to the clause. + * @param string clause name + * @param array arguments + * @return DibiFluent provides a fluent interface + */ + public function __call($clause, $args) + { + $clause = self::_clause($clause); + + // lazy initialization + if ($this->command === NULL) { + if (isset(self::$masks[$clause])) { + $this->clauses = array_fill_keys(self::$masks[$clause], NULL); + } + $this->cursor = & $this->clauses[$clause]; + $this->cursor = array(); + $this->command = $clause; + } + + // special types or argument + if (count($args) === 1) { + $arg = $args[0]; + if ($arg === TRUE) { + $args = array(); + + } elseif (is_string($arg) && preg_match('#^[a-z][a-z0-9_.]*$#i', $arg)) { + $args = array('%n', $arg); + } + } + + if (array_key_exists($clause, $this->clauses)) { + // append to clause + $this->cursor = & $this->clauses[$clause]; + + if ($args === array(FALSE)) { + $this->cursor = NULL; + return $this; + } + + if (isset(self::$separators[$clause])) { + $sep = self::$separators[$clause]; + if ($sep === FALSE) { + $this->cursor = array(); + + } elseif (!empty($this->cursor)) { + $this->cursor[] = $sep; + } + } + + } else { + // append to currect flow + if ($args === array(FALSE)) { + return $this; + } + + $this->cursor[] = $clause; + } + + if ($this->cursor === NULL) { + $this->cursor = array(); + } + + array_splice($this->cursor, count($this->cursor), 0, $args); + return $this; + } + + + + /** + * Switch to a clause. + * @param string clause name + * @return DibiFluent provides a fluent interface + */ + public function clause($clause, $remove = FALSE) + { + $this->cursor = & $this->clauses[self::_clause($clause)]; + + if ($remove) { + $this->cursor = NULL; + + } elseif ($this->cursor === NULL) { + $this->cursor = array(); + } + + return $this; + } + + + + /** + * Change a SQL flag. + * @param string flag name + * @param bool value + * @return DibiFluent provides a fluent interface + */ + public function setFlag($flag, $value = TRUE) + { + $flag = strtoupper($flag); + if ($value) { + $this->flags[$flag] = TRUE; + } else { + unset($this->flags[$flag]); + } + return $this; + } + + + + /** + * Is a flag set? + * @param string flag name + * @return bool + */ + final public function getFlag($flag, $value = TRUE) + { + return isset($this->flags[strtoupper($flag)]); + } + + + + /** + * Returns SQL command. + * @return string + */ + final public function getCommand() + { + return $this->command; + } + + + + /** + * Generates and executes SQL query. + * @return DibiResult|NULL result set object (if any) + * @throws DibiException + */ + public function execute() + { + return $this->connection->query($this->_export()); + } + + + + /** + * Generates and prints SQL query or it's part. + * @param string clause name + * @return bool + */ + public function test($clause = NULL) + { + return $this->connection->test($this->_export($clause)); + } + + + + /** + * Generates parameters for DibiTranslator. + * @param string clause name + * @return array + */ + protected function _export($clause = NULL) + { + if ($clause === NULL) { + $data = $this->clauses; + + } else { + $clause = self::_clause($clause); + if (array_key_exists($clause, $this->clauses)) { + $data = array($clause => $this->clauses[$clause]); + } else { + return array(); + } + } + + $args = array(); + foreach ($data as $clause => $statement) { + if ($statement !== NULL) { + if ($clause[0] !== '%') { + $args[] = $clause; + if ($clause === $this->command) { + $args[] = implode(' ', array_keys($this->flags)); + } + } + array_splice($args, count($args), 0, $statement); + } + } + return $args; + } + + + + /** + * Format camelCase clause name to UPPER CASE. + * @param string + * @return string + */ + private static function _clause($s) + { + if ($s === 'order' || $s === 'group') { + $s .= 'By'; + trigger_error("Did you mean '$s'?", E_USER_NOTICE); + } + return strtoupper(preg_replace('#[A-Z]#', ' $0', $s)); + + } + + + + /** + * Returns (highlighted) SQL query. + * @return string + */ + final public function __toString() + { + ob_start(); + $this->test(); + return ob_get_clean(); + } + +} + + +// PHP < 5.2 compatibility +if (!function_exists('array_fill_keys')) { + function array_fill_keys($keys, $value) + { + return array_combine($keys, array_fill(0, count($keys), $value)); + } +} diff --git a/dibi/libs/DibiResult.php b/dibi/libs/DibiResult.php index b19a7f53..b6c8a048 100644 --- a/dibi/libs/DibiResult.php +++ b/dibi/libs/DibiResult.php @@ -495,13 +495,36 @@ class DibiResult extends /*Nette::*/Object implements IDataSource - final public function setType($col, $type = NULL, $format = NULL) + /** + * Define column type. + * @param string column + * @param string type (use constant Dibi::FIELD_*) + * @param string optional format + * @return void + */ + final public function setType($col, $type, $format = NULL) { $this->xlat[$col] = array($type, $format); } + /** + * Define multiple columns types (for internal usage). + * @param array + * @return void + */ + final public function setTypes(array $types) + { + $this->xlat = $types; + } + + + + /** + * Returns column type. + * @return array ($type, $format) + */ final public function getType($col) { return isset($this->xlat[$col]) ? $this->xlat[$col] : NULL; @@ -509,6 +532,10 @@ class DibiResult extends /*Nette::*/Object implements IDataSource + /** + * Converts value to specified type and format + * @return array ($type, $format) + */ final public function convert($value, $type, $format = NULL) { if ($value === NULL || $value === FALSE) { diff --git a/dibi/libs/DibiTranslator.php b/dibi/libs/DibiTranslator.php index 471e7c81..a6081c89 100644 --- a/dibi/libs/DibiTranslator.php +++ b/dibi/libs/DibiTranslator.php @@ -130,7 +130,7 @@ final class DibiTranslator extends /*Nette::*/Object %([a-zA-Z]{1,4})(?![a-zA-Z])|## 8) modifier )/xs', */ // note: this can change $this->args & $this->cursor & ... - . preg_replace_callback('/(?=`|\[|\'|"|%)(?:`(.+?)`|\[(.+?)\]|(\')((?:\'\'|[^\'])*)\'|(")((?:""|[^"])*)"|(\'|")|%([a-zA-Z]{1,4})(?![a-zA-Z]))/s', + . preg_replace_callback('/(?=`|\[|\'|"|%)(?:`(.+?)`|\[(.+?)\]|(\')((?:\'\'|[^\'])*)\'|(")((?:""|[^"])*)"|(\'|")|%([a-zA-Z]{1,4})(?![a-zA-Z]))/s', array($this, 'cb'), substr($arg, $toSkip) ); @@ -219,7 +219,7 @@ final class DibiTranslator extends /*Nette::*/Object return implode($separator, $vx); - case 'l': // LIST val, val, ... + case 'l': // LIST (val, val, ...) foreach ($value as $k => $v) { $pair = explode('%', $k, 2); // split into identifier & modifier $vx[] = $this->formatValue($v, isset($pair[1]) ? $pair[1] : FALSE); @@ -301,7 +301,7 @@ final class DibiTranslator extends /*Nette::*/Object return $value; } else { return substr($value, 0, $toSkip) - . preg_replace_callback('/(?=`|\[|\'|")(?:`(.+?)`|\[(.+?)\]|(\')((?:\'\'|[^\'])*)\'|(")((?:""|[^"])*)"(\'|"))/s', + . preg_replace_callback('/(?=`|\[|\'|")(?:`(.+?)`|\[(.+?)\]|(\')((?:\'\'|[^\'])*)\'|(")((?:""|[^"])*)"(\'|"))/s', array($this, 'cb'), substr($value, $toSkip) ); diff --git a/examples/fluent.test.php b/examples/fluent.test.php new file mode 100644 index 00000000..0126f625 --- /dev/null +++ b/examples/fluent.test.php @@ -0,0 +1,63 @@ +