diff --git a/app/Config.example.php b/app/Config.example.php index 07de120ff0..5abf9aeae8 100644 --- a/app/Config.example.php +++ b/app/Config.example.php @@ -96,6 +96,21 @@ */ // define('SITEEMAIL', 'email@domain.com'); +/** + * Setup the Database configuration. + */ +Config::set('database', array( + 'default' => array( + 'driver' => DB_TYPE, + 'hostname' => DB_HOST, + 'database' => DB_NAME, + 'username' => DB_USER, + 'password' => DB_PASS, + 'charset' => 'utf8', + 'collation' => 'utf8_general_ci', + ), +)); + /** * Setup the (class) Aliases configuration. */ @@ -126,6 +141,6 @@ 'SimpleCurl' => '\Helpers\SimpleCurl', 'TableBuilder' => '\Helpers\TableBuilder', 'Tags' => '\Helpers\Tags', - 'Url' => '\Helpers\Url' + 'Url' => '\Helpers\Url', + 'DB' => '\Database\Facade', )); - diff --git a/app/Templates/Default/default.php b/app/Templates/Default/default.php index 79f6c93876..315d8f1daf 100644 --- a/app/Templates/Default/default.php +++ b/app/Templates/Default/default.php @@ -29,6 +29,7 @@ '>German | '>French | '>Italian | + '>Japanese | '>Dutch | '>Persian | '>Polish | diff --git a/system/Database/Connection.php b/system/Database/Connection.php new file mode 100644 index 0000000000..54d36f1ac6 --- /dev/null +++ b/system/Database/Connection.php @@ -0,0 +1,242 @@ +tablePrefix = $config['prefix']; + + // Create the PDO instance from the given configuration. + extract($config); + + $dsn = "$driver:host={$hostname};dbname={$database}"; + + $options = array( + PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES {$charset} COLLATE {$collation}" + ); + + $this->pdo = new PDO($dsn, $username, $password, $options); + } + + /** + * Get an instance of the Database Connection. + * + * @param $name string Name of the connection provided in the configuration + * @return Connection|\PDO|null + * @throws \Exception + */ + public static function getInstance($name = 'default') + { + if (isset(static::$instances[$name])) { + // When already have an Connection instance, return it. + return static::$instances[$name]; + } + + // Get the requested Connection options. + $config = Config::get('database'); + + if (isset($config[$name])) { + // Create the Connection instance. + static::$instances[$name] = new static($config[$name]); + + // Return the Connection instance. + return static::$instances[$name]; + } + + throw new \Exception("Connection name '$name' is not defined in your configuration"); + } + + /** + * Begin a fluent query against a database table. + * + * @param string $table + * @return \Database\Query + */ + public function table($table) + { + $query = new Query($this); + + return $query->from($table); + } + + /** + * Run a select statement against the database. + * + * @param string $query + * @param array $bindings + * @return array + */ + public function select($query, $bindings = array()) + { + $statement = $this->getPdo()->prepare($query); + + $statement->execute($bindings); + + return $statement->fetchAll($this->getFetchMode()); + } + + /** + * Run an insert statement against the database. + * + * @param string $query + * @param array $bindings + * @return bool + */ + public function insert($query, $bindings = array()) + { + return $this->statement($query, $bindings); + } + + /** + * Run an update statement against the database. + * + * @param string $query + * @param array $bindings + * @return int + */ + public function update($query, $bindings = array()) + { + return $this->affectingStatement($query, $bindings); + } + + /** + * Run a delete statement against the database. + * + * @param string $query + * @param array $bindings + * @return int + */ + public function delete($query, $bindings = array()) + { + return $this->affectingStatement($query, $bindings); + } + + /** + * Execute an SQL statement and return the boolean result. + * + * @param string $query + * @param array $bindings + * @return bool + */ + public function statement($query, $bindings = array()) + { + return $this->getPdo()->prepare($query)->execute($bindings); + } + + /** + * Run an SQL statement and get the number of rows affected. + * + * @param string $query + * @param array $bindings + * @return int + */ + public function affectingStatement($query, $bindings = array()) + { + $statement = $this->getPdo()->prepare($query); + + $statement->execute($bindings); + + return $statement->rowCount(); + } + + /** + * Get the table prefix for the connection. + * + * @return string + */ + public function getTablePrefix() + { + return $this->tablePrefix; + } + + /** + * Set the table prefix in use by the connection. + * + * @param string $prefix + * @return void + */ + public function setTablePrefix($prefix) + { + $this->tablePrefix = $prefix; + } + + /** + * Get the PDO instance. + * + * @return PDO + */ + public function getPdo() + { + return $this->pdo; + } + + /** + * Get the default fetch mode for the connection. + * + * @return int + */ + public function getFetchMode() + { + return $this->fetchMode; + } + + /** + * Set the default fetch mode for the connection. + * + * @param int $fetchMode + * @return int + */ + public function setFetchMode($fetchMode) + { + $this->fetchMode = $fetchMode; + } +} diff --git a/system/Database/Facade.php b/system/Database/Facade.php new file mode 100644 index 0000000000..fb8a13d85a --- /dev/null +++ b/system/Database/Facade.php @@ -0,0 +1,29 @@ +type = $type; + $this->query = $query; + $this->table = $table; + } + + /** + * Add an "ON" clause to the join. + * + * @param string $first + * @param string $operator + * @param string $second + * @param string $boolean + * @param bool $where + * @return \Database\JoinClause + */ + public function on($first, $operator, $second, $boolean = 'and', $where = false) + { + $this->clauses[] = compact('first', 'operator', 'second', 'boolean', 'where'); + + if ($where) $this->query->addBinding($second); + + return $this; + } + + /** + * Add an "OR ON" clause to the join. + * + * @param string $first + * @param string $operator + * @param string $second + * @return \Database\JoinClause + */ + public function orOn($first, $operator, $second) + { + return $this->on($first, $operator, $second, 'or'); + } + + /** + * Add an "ON WHERE" clause to the join. + * + * @param string $first + * @param string $operator + * @param string $second + * @param string $boolean + * @return \Database\JoinClause + */ + public function where($first, $operator, $second, $boolean = 'and') + { + return $this->on($first, $operator, $second, $boolean, true); + } + + /** + * Add an "OR ON WHERE" clause to the join. + * + * @param string $first + * @param string $operator + * @param string $second + * @param string $boolean + * @return \Database\JoinClause + */ + public function orWhere($first, $operator, $second) + { + return $this->on($first, $operator, $second, 'or', true); + } +} diff --git a/system/Database/Model.php b/system/Database/Model.php new file mode 100644 index 0000000000..c4745595a4 --- /dev/null +++ b/system/Database/Model.php @@ -0,0 +1,101 @@ +table) { + // Not Table name specified? Try to auto-calculate it. + $className = get_class($this); + + $this->table = Inflector::tableize(class_basename($className)); + } + + $this->db = Connection::getInstance(); + } + + /** + * Get the Table for the Model. + * + * @return string + */ + public function getTable() + { + return $this->table; + } + + /** + * Get the Primary Key for the Model. + * + * @return string + */ + public function getKeyName() + { + return $this->primaryKey; + } + + /** + * Get a new Query for the Model's table. + * + * @return \Database\Query + */ + public function newQuery() + { + return $this->db + ->table($this->table) + ->setModel($this); + } + + /** + * Handle dynamic method calls into the method. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + $query = $this->newQuery(); + + return call_user_func_array(array($query, $method), $parameters); + } +} diff --git a/system/Database/Query.php b/system/Database/Query.php new file mode 100644 index 0000000000..0188b2fbbb --- /dev/null +++ b/system/Database/Query.php @@ -0,0 +1,1546 @@ +', '<=', '>=', '<>', '!=', 'like', 'not like', 'between', 'ilike', '&', '|', '^', '<<', '>>', + ); + + /** + * Create a new query instance. + * + * @return void + */ + public function __construct(Connection $db) + { + $this->db = $db; + } + + /** + * Set the columns to be selected. + * + * @param array $columns + * @return \Database\Query + */ + public function select($columns = array('*')) + { + $this->columns = is_array($columns) ? $columns : func_get_args(); + + return $this; + } + + /** + * Force the query to only return distinct results. + * + * @return \Database\Query + */ + public function distinct() + { + $this->distinct = true; + + return $this; + } + + /** + * Set the table which the query is targeting. + * + * @param string $table + * @return \Database\Query + */ + public function from($table) + { + $this->from = $table; + + return $this; + } + + /** + * Add a join clause to the query. + * + * @param string $table + * @param string $first + * @param string $operator + * @param string $two + * @param string $type + * @param bool $where + * @return \Database\Query + */ + public function join($table, $one, $operator = null, $two = null, $type = 'inner', $where = false) + { + if ($one instanceof Closure) { + $this->joins[] = new JoinClause($this, $type, $table); + + call_user_func($one, end($this->joins)); + } else { + $join = new JoinClause($this, $type, $table); + + $this->joins[] = $join->on( + $one, $operator, $two, 'and', $where + ); + } + + return $this; + } + + /** + * Add a "JOIN WHERE" clause to the query. + * + * @param string $table + * @param string $first + * @param string $operator + * @param string $two + * @param string $type + * @return \Database\Query + */ + public function joinWhere($table, $one, $operator, $two, $type = 'inner') + { + return $this->join($table, $one, $operator, $two, $type, true); + } + + /** + * Add a left join to the query. + * + * @param string $table + * @param string $first + * @param string $operator + * @param string $second + * @return \Database\Query + */ + public function leftJoin($table, $first, $operator = null, $second = null) + { + return $this->join($table, $first, $operator, $second, 'left'); + } + + /** + * Add a "JOIN WHERE" clause to the query. + * + * @param string $table + * @param string $first + * @param string $operator + * @param string $two + * @return \Database\Query + */ + public function leftJoinWhere($table, $one, $operator, $two) + { + return $this->joinWhere($table, $one, $operator, $two, 'left'); + } + + /** + * Add a basic where clause to the query. + * + * @param string $column + * @param string $operator + * @param mixed $value + * @param string $boolean + * @return \Database\Query + * + * @throws \InvalidArgumentException + */ + public function where($column, $operator = null, $value = null, $boolean = 'and') + { + if (func_num_args() == 2) { + list($value, $operator) = array($operator, '='); + } else if ($this->invalidOperatorAndValue($operator, $value)) { + throw new \InvalidArgumentException("Value must be provided."); + } + + if ($column instanceof Closure) { + return $this->whereNested($column, $boolean); + } + + if (! in_array(strtolower($operator), $this->operators, true)) { + list($value, $operator) = array($operator, '='); + } + + if ($value instanceof Closure) { + return $this->whereSub($column, $operator, $value, $boolean); + } + + if (is_null($value)) { + return $this->whereNull($column, $boolean, $operator != '='); + } + + $type = 'Basic'; + + $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean'); + + $this->bindings[] = $value; + + return $this; + } + + /** + * Add an "OR WHERE" clause to the query. + * + * @param string $column + * @param string $operator + * @param mixed $value + * @return \Database\Query + */ + public function orWhere($column, $operator = null, $value = null) + { + return $this->where($column, $operator, $value, 'or'); + } + + /** + * Determine if the given operator and value combination is legal. + * + * @param string $operator + * @param mixed $value + * @return bool + */ + protected function invalidOperatorAndValue($operator, $value) + { + $isOperator = in_array($operator, $this->operators); + + return ($isOperator && ($operator != '=') && is_null($value)); + } + + /** + * Add a where between statement to the query. + * + * @param string $column + * @param array $values + * @param string $boolean + * @param bool $not + * @return \Database\Query + */ + public function whereBetween($column, array $values, $boolean = 'and', $not = false) + { + $type = 'between'; + + $this->wheres[] = compact('column', 'type', 'boolean', 'not'); + + $this->bindings = array_merge($this->bindings, $values); + + return $this; + } + + /** + * Add an or where between statement to the query. + * + * @param string $column + * @param array $values + * @return \Database\Query + */ + public function orWhereBetween($column, array $values) + { + return $this->whereBetween($column, $values, 'or'); + } + + /** + * Add a where not between statement to the query. + * + * @param string $column + * @param array $values + * @param string $boolean + * @return \Database\Query + */ + public function whereNotBetween($column, array $values, $boolean = 'and') + { + return $this->whereBetween($column, $values, $boolean, true); + } + + /** + * Add an or where not between statement to the query. + * + * @param string $column + * @param array $values + * @return \Database\Query + */ + public function orWhereNotBetween($column, array $values) + { + return $this->whereNotBetween($column, $values, 'or'); + } + + /** + * Add a nested where statement to the query. + * + * @param \Closure $callback + * @param string $boolean + * @return \Database\Query + */ + public function whereNested(Closure $callback, $boolean = 'and') + { + $query = $this->newQuery(); + + $query->from($this->from); + + call_user_func($callback, $query); + + return $this->addNestedWhereQuery($query, $boolean); + } + + /** + * Add another query builder as a nested where to the query builder. + * + * @param \Database\Query $query + * @param string $boolean + * @return \Database\Query + */ + public function addNestedWhereQuery($query, $boolean = 'and') + { + if (count($query->wheres)) { + $type = 'Nested'; + + $this->wheres[] = compact('type', 'query', 'boolean'); + + $this->mergeBindings($query); + } + + return $this; + } + + /** + * Add a full sub-select to the query. + * + * @param string $column + * @param string $operator + * @param \Closure $callback + * @param string $boolean + * @return \Database\Query + */ + protected function whereSub($column, $operator, Closure $callback, $boolean) + { + $type = 'Sub'; + + $query = $this->newQuery(); + + call_user_func($callback, $query); + + $this->wheres[] = compact('type', 'column', 'operator', 'query', 'boolean'); + + $this->mergeBindings($query); + + return $this; + } + + /** + * Add a "WHERE IN" clause to the query. + * + * @param string $column + * @param mixed $values + * @param string $boolean + * @param bool $not + * @return \Database\Query + */ + public function whereIn($column, $values, $boolean = 'and', $not = false) + { + $type = $not ? 'NotIn' : 'In'; + + if ($values instanceof Closure) { + return $this->whereInSub($column, $values, $boolean, $not); + } + + $this->wheres[] = compact('type', 'column', 'values', 'boolean'); + + $this->bindings = array_merge($this->bindings, $values); + + return $this; + } + + /** + * Add an "OR WHERE IN" clause to the query. + * + * @param string $column + * @param mixed $values + * @return \Database\Query + */ + public function orWhereIn($column, $values) + { + return $this->whereIn($column, $values, 'or'); + } + + /** + * Add a "WHERE NOT IN" clause to the query. + * + * @param string $column + * @param mixed $values + * @param string $boolean + * @return \Database\Query + */ + public function whereNotIn($column, $values, $boolean = 'and') + { + return $this->whereIn($column, $values, $boolean, true); + } + + /** + * Add an "OR WHERE NOT IN" clause to the query. + * + * @param string $column + * @param mixed $values + * @return \Database\Query + */ + public function orWhereNotIn($column, $values) + { + return $this->whereNotIn($column, $values, 'or'); + } + + /** + * Add a where in with a sub-select to the query. + * + * @param string $column + * @param \Closure $callback + * @param string $boolean + * @param bool $not + * @return \Database\Query + */ + protected function whereInSub($column, Closure $callback, $boolean, $not) + { + $type = $not ? 'NotInSub' : 'InSub'; + + call_user_func($callback, $query = $this->newQuery()); + + $this->wheres[] = compact('type', 'column', 'query', 'boolean'); + + $this->mergeBindings($query); + + return $this; + } + + /** + * Add a "WHERE NULL" clause to the query. + * + * @param string $column + * @param string $boolean + * @param bool $not + * @return \Database\Query + */ + public function whereNull($column, $boolean = 'and', $not = false) + { + $type = $not ? 'NotNull' : 'Null'; + + $this->wheres[] = compact('type', 'column', 'boolean'); + + return $this; + } + + /** + * Add an "OR WHERE NULL" clause to the query. + * + * @param string $column + * @return \Database\Query + */ + public function orWhereNull($column) + { + return $this->whereNull($column, 'or'); + } + + /** + * Add a "WHERE NOT NULL" clause to the query. + * + * @param string $column + * @param string $boolean + * @return \Database\Query + */ + public function whereNotNull($column, $boolean = 'and') + { + return $this->whereNull($column, $boolean, true); + } + + /** + * Add an "OR WHERE NOT NULL" clause to the query. + * + * @param string $column + * @return \Database\Query + */ + public function orWhereNotNull($column) + { + return $this->whereNotNull($column, 'or'); + } + + /** + * Add an "ORDER BY" clause to the query. + * + * @param string $column + * @param string $direction + * @return \Database\Query + */ + public function orderBy($column, $direction = 'asc') + { + $direction = strtolower($direction) == 'asc' ? 'asc' : 'desc'; + + $this->orders[] = compact('column', 'direction'); + + return $this; + } + + /** + * Set the "OFFSET" value of the query. + * + * @param int $value + * @return \Database\Query + */ + public function offset($value) + { + $this->offset = max(0, $value); + + return $this; + } + + /** + * Alias to set the "OFFSET" value of the query. + * + * @param int $value + * @return \Database\Query + */ + public function skip($value) + { + return $this->offset($value); + } + + /** + * Set the "LIMIT" value of the query. + * + * @param int $value + * @return \Database\Query + */ + public function limit($value) + { + if ($value > 0) { + $this->limit = $value; + } + + return $this; + } + + /** + * Alias to set the "LIMIT" value of the query. + * + * @param int $value + * @return \Database\Query + */ + public function take($value) + { + return $this->limit($value); + } + + /** + * Execute a query for a single record by ID. + * + * @param int $id + * @param array $columns + * @return mixed + */ + public function find($id, $columns = array('*')) + { + $uniqueIdentifier = isset($this->model) ? $this->model->getKeyName() : 'id'; + + return $this->where($uniqueIdentifier, '=', $id)->first($columns); + } + + /** + * Pluck a single column's value from the first result of a query. + * + * @param string $column + * @return mixed + */ + public function pluck($column) + { + $result = $this->first(array($column)); + + if (! is_null($result)) { + $result = (array) $result; + } + + return (count($result) > 0) ? reset($result) : null; + } + + /** + * Execute the query and get the first result. + * + * @param array $columns + * @return mixed + */ + public function first($columns = array('*')) + { + $results = $this->take(1)->get($columns); + + return (count($results) > 0) ? reset($results) : null; + } + + /** + * Execute the query as a "SELECT" statement. + * + * @param array $columns + * @return array + */ + public function get($columns = array('*')) + { + if (is_null($this->columns)) { + $this->columns = $columns; + } + + $results = $this->runSelect(); + + return $results; + } + + /** + * Run the query as a "SELECT" statement against the connection. + * + * @return array + */ + protected function runSelect() + { + return $this->db->select($this->toSql(), $this->bindings); + } + + /** + * Get the SQL representation of the query. + * + * @return string + */ + public function toSql() + { + return $this->compileSelect($this); + } + + /** + * Determine if any rows exist for the current query. + * + * @return bool + */ + public function exists() + { + return $this->count() > 0; + } + + /** + * Retrieve the "COUNT" result of the query. + * + * @param string $column + * @return int + */ + public function count($column = '*') + { + return (int) $this->aggregate(__FUNCTION__, array($column)); + } + + /** + * Retrieve the minimum value of a given column. + * + * @param string $column + * @return mixed + */ + public function min($column) + { + return $this->aggregate(__FUNCTION__, array($column)); + } + + /** + * Retrieve the maximum value of a given column. + * + * @param string $column + * @return mixed + */ + public function max($column) + { + return $this->aggregate(__FUNCTION__, array($column)); + } + + /** + * Retrieve the sum of the values of a given column. + * + * @param string $column + * @return mixed + */ + public function sum($column) + { + return $this->aggregate(__FUNCTION__, array($column)); + } + + /** + * Retrieve the average of the values of a given column. + * + * @param string $column + * @return mixed + */ + public function avg($column) + { + return $this->aggregate(__FUNCTION__, array($column)); + } + + /** + * Execute an aggregate function on the database. + * + * @param string $function + * @param array $columns + * @return mixed + */ + public function aggregate($function, $columns = array('*')) + { + $this->aggregate = compact('function', 'columns'); + + $results = $this->get($columns); + + $this->columns = null; + + $this->aggregate = null; + + if (isset($results[0])) { + $result = $results[0]; + + $result = (array) $result; + + return $result['aggregate']; + } + } + + /** + * Insert a new record into the database. + * + * @param array $values + * @return bool + */ + public function insert(array $values) + { + if (!is_array(reset($values))) { + $values = array($values); + } else { + foreach ($values as $key => $value) { + ksort($value); + + $values[$key] = $value; + } + } + + $bindings = array(); + + foreach ($values as $record) { + $bindings = array_merge($bindings, array_values($record)); + } + + $sql = $this->compileInsert($values); + + $bindings = $this->cleanBindings($bindings); + + return $this->db->insert($sql, $bindings); + } + + /** + * Replace a new record into the database. + * + * @param array $values + * @return bool + */ + public function replace(array $values) + { + if (!is_array(reset($values))) { + $values = array($values); + } else { + foreach ($values as $key => $value) { + ksort($value); + + $values[$key] = $value; + } + } + + $bindings = array(); + + foreach ($values as $record) { + $bindings = array_merge($bindings, array_values($record)); + } + + $sql = $this->compileReplace($values); + + $bindings = $this->cleanBindings($bindings); + + return $this->db->insert($sql, $bindings); + } + + /** + * Insert a new record and get the value of the primary key. + * + * @param array $values + * @return int + */ + public function insertGetId(array $values) + { + $sql = $this->compileInsert($values); + + $values = $this->cleanBindings($values); + + $this->db->insert($sql, $values); + + $id = $this->db->getPdo()->lastInsertId(); + + return is_numeric($id) ? (int) $id : $id; + } + + /** + * Update a record in the database. + * + * @param array $values + * @return int + */ + public function update(array $values) + { + $bindings = array_values(array_merge($values, $this->bindings)); + + $sql = $this->compileUpdate($values); + + return $this->db->update($sql, $this->cleanBindings($bindings)); + } + + /** + * Delete a record from the database. + * + * @param mixed $id + * @return int + */ + public function delete($id = null) + { + $uniqueIdentifier = isset($this->model) ? $this->model->getKeyName() : 'id'; + + if (! is_null($id)) { + $this->where($uniqueIdentifier, '=', $id); + } + + $sql = $this->compileDelete(); + + return $this->db->delete($sql, $this->bindings); + } + + /** + * Run a truncate statement on the table. + * + * @return void + */ + public function truncate() + { + foreach ($this->compileTruncate() as $sql => $bindings) { + $this->db->statement($sql, $bindings); + } + } + + /** + * Get a new instance of the query builder. + * + * @return \Database\Query + */ + public function newQuery() + { + return new Query($this->db); + } + + /** + * Merge an array of bindings into our bindings. + * + * @param \Database\Query $query + * @return \Database\Query + */ + public function mergeBindings(Query $query) + { + $this->bindings = array_values(array_merge($this->bindings, $query->bindings)); + + return $this; + } + + /** + * Remove all of the expressions from a list of bindings. + * + * @param array $bindings + * @return array + */ + protected function cleanBindings(array $bindings) + { + return array_values(array_filter($bindings, function($binding) { + return true; + })); + } + + /** + * Set a model instance for the model being queried. + * + * @param \Database\Model|null $model + * @return \Database\Query + */ + public function setModel($model) + { + $this->model = $model; + + return $this; + } + + /** + * Compile a select query into SQL. + * + * @param \Database\Query $query + * @return string + */ + public function compileSelect(Query $query) + { + if (is_null($query->columns)) { + $query->columns = array('*'); + } + + return trim($this->concatenate($this->compileComponents($query))); + } + + /** + * Compile the components necessary for a select clause. + * + * @param \Database\Query $query + * @return array + */ + protected function compileComponents(Query $query) + { + $sql = array(); + + $selectComponents = array( + 'aggregate', + 'columns', + 'from', + 'joins', + 'wheres', + 'orders', + 'limit', + 'offset' + ); + + foreach ($selectComponents as $component) { + if (!is_null($query->$component)) { + $method = 'compile' .ucfirst($component); + + $sql[$component] = $this->$method($query, $query->$component); + } + } + + return $sql; + } + + /** + * Compile an aggregated select clause. + * + * @param \Database\Query $query + * @param array $aggregate + * @return string + */ + protected function compileAggregate(Query $query, $aggregate) + { + $column = $this->columnize($aggregate['columns']); + + if ($query->distinct && $column !== '*') { + $column = 'DISTINCT '.$column; + } + + return 'SELECT ' .$aggregate['function'] .'(' .$column .') AS AGGREGATE'; + } + + /** + * Compile the "SELECT *" portion of the query. + * + * @param \Database\Query $query + * @param array $columns + * @return string + */ + protected function compileColumns(Query $query, $columns) + { + if (is_null($query->aggregate)) { + $select = $query->distinct ? 'SELECT DISTINCT ' : 'SELECT '; + + return $select .$this->columnize($columns); + } + + return ''; + } + + /** + * Compile the "FROM" portion of the query. + * + * @param \Database\Query $query + * @param string $table + * @return string + */ + protected function compileFrom(Query $query, $table) + { + return 'FROM '.$this->wrapTable($table); + } + + /** + * Compile the "JOIN" portions of the query. + * + * @param \Database\Query $query + * @param array $joins + * @return string + */ + protected function compileJoins(Query $query, $joins) + { + $sql = array(); + + foreach ($joins as $join) { + $table = $this->wrapTable($join->table); + + $clauses = array(); + + foreach ($join->clauses as $clause) { + $clauses[] = $this->compileJoinConstraint($clause); + } + + $clauses[0] = $this->removeLeadingBoolean($clauses[0]); + + $clauses = implode(' ', $clauses); + + $type = $join->type; + + $sql[] = "$type JOIN $table ON $clauses"; + } + + return implode(' ', $sql); + } + + /** + * Create a join clause constraint segment. + * + * @param array $clause + * @return string + */ + protected function compileJoinConstraint(array $clause) + { + $first = $this->wrap($clause['first']); + + $second = $clause['where'] ? '?' : $this->wrap($clause['second']); + + return "{$clause['boolean']} $first {$clause['operator']} $second"; + } + + /** + * Compile the "WHERE" portions of the query. + * + * @param \Database\Query $query + * @return string + */ + protected function compileWheres(Query $query) + { + $sql = array(); + + if (is_null($query->wheres)) { + return ''; + } + + foreach ($query->wheres as $where) { + $method = "compileWhere{$where['type']}"; + + $sql[] = $where['boolean'].' '.$this->$method($where); + } + + if (count($sql) > 0) { + $sql = implode(' ', $sql); + + return 'WHERE ' .preg_replace('/AND |OR /', '', $sql, 1); + } + + return ''; + } + + /** + * Compile a nested where clause. + * + * @param array $where + * @return string + */ + protected function compileWhereNested($where) + { + $nested = $where['query']; + + return '(' .substr($this->compileWheres($nested), 6) .')'; + } + + /** + * Compile a where condition with a sub-select. + * + * @param array $where + * @return string + */ + protected function compileWhereSub($where) + { + $select = $this->compileSelect($where['query']); + + return $this->wrap($where['column']) .' ' .$where['operator'] ." ($select)"; + } + + /** + * Compile a basic where clause. + * + * @param array $where + * @return string + */ + protected function compileWhereBasic($where) + { + $value = $this->parameter($where['value']); + + return $this->wrap($where['column']) .' ' .$where['operator'] .' ' .$value; + } + + /** + * Compile a "BETWEEN" where clause. + * + * @param array $where + * @return string + */ + protected function compileWhereBetween($where) + { + $between = $where['not'] ? 'NOT BETWEEN' : 'BETWEEN'; + + return $this->wrap($where['column']) .' ' .$between .' ? AND ?'; + } + + /** + * Compile a "WHERE IN" clause. + * + * @param array $where + * @return string + */ + protected function compileWhereIn($where) + { + $values = $this->parameterize($where['values']); + + return $this->wrap($where['column']) .' IN (' .$values .')'; + } + + /** + * Compile a "WHERE NOT IN" clause. + * + * @param array $where + * @return string + */ + protected function compileWhereNotIn($where) + { + $values = $this->parameterize($where['values']); + + return $this->wrap($where['column']) .' NOT IN (' .$values .')'; + } + + /** + * Compile a where in sub-select clause. + * + * @param array $where + * @return string + */ + protected function compileWhereInSub($where) + { + $select = $this->compileSelect($where['query']); + + return $this->wrap($where['column']) .' IN (' .$select .')'; + } + + /** + * Compile a where not in sub-select clause. + * + * @param array $where + * @return string + */ + protected function compileWhereNotInSub($where) + { + $select = $this->compileSelect($where['query']); + + return $this->wrap($where['column']) .' NOT IN (' .$select .')'; + } + + /** + * Compile a "WHERE NULL" clause. + * + * @param array $where + * @return string + */ + protected function compileWhereNull($where) + { + return $this->wrap($where['column']) .' IS NULL'; + } + + /** + * Compile a "WHERE NOT NULL" clause. + * + * @param array $where + * @return string + */ + protected function compileWhereNotNull($where) + { + return $this->wrap($where['column']) .' IS NOT NULL'; + } + + /** + * Compile the "ORDER BY" portions of the query. + * + * @param \Database\Query $query + * @param array $orders + * @return string + */ + protected function compileOrders(Query $query, $orders) + { + $me = $this; + + return 'ORDER BY ' .implode(', ', array_map(function($order) use ($me) { + if (isset($order['sql'])) { + return $order['sql']; + } + + return $me->wrap($order['column']).' '.$order['direction']; + }, $orders)); + } + + /** + * Compile the "LIMIT" portions of the query. + * + * @param \Database\Query $query + * @param int $limit + * @return string + */ + protected function compileLimit(Query $query, $limit) + { + return 'LIMIT ' .(int) $limit; + } + + /** + * Compile the "OFFSET" portions of the query. + * + * @param \Database\Query $query + * @param int $offset + * @return string + */ + protected function compileOffset(Query $query, $offset) + { + return 'OFFSET ' .(int) $offset; + } + + /** + * Compile an insert statement into SQL. + * + * @param array $values + * @return string + */ + public function compileInsert(array $values) + { + $table = $this->wrapTable($this->from); + + if (! is_array(reset($values))) { + $values = array($values); + } + + $columns = $this->columnize(array_keys(reset($values))); + + $parameters = $this->parameterize(reset($values)); + + $value = array_fill(0, count($values), "($parameters)"); + + $parameters = implode(', ', $value); + + return "INSERT INTO $table ($columns) VALUES $parameters"; + } + + /** + * Compile an replace statement into SQL. + * + * @param array $values + * @return string + */ + public function compileReplace(array $values) + { + $table = $this->wrapTable($this->from); + + if (! is_array(reset($values))) { + $values = array($values); + } + + $columns = $this->columnize(array_keys(reset($values))); + + $parameters = $this->parameterize(reset($values)); + + $value = array_fill(0, count($values), "($parameters)"); + + $parameters = implode(', ', $value); + + return "REPLACE INTO $table ($columns) VALUES $parameters"; + } + + /** + * Compile an update statement into SQL. + * + * @param array $values + * @return string + */ + public function compileUpdate($values) + { + $table = $this->wrapTable($this->from); + + $columns = array(); + + foreach ($values as $key => $value) { + $columns[] = $this->wrap($key) .' = ' .$this->parameter($value); + } + + $columns = implode(', ', $columns); + + if (isset($this->joins)) { + $joins = ' ' .$this->compileJoins($this, $this->joins); + } else { + $joins = ''; + } + + $where = $this->compileWheres($this); + + $sql = trim("UPDATE {$table}{$joins} SET $columns $where"); + + if (isset($this->orders)) { + $sql .= ' ' .$this->compileOrders($this, $this->orders); + } + + if (isset($this->limit)) { + $sql .= ' ' .$this->compileLimit($this, $this->limit); + } + + return rtrim($sql); + } + + /** + * Compile a delete statement into SQL. + * + * @return string + */ + public function compileDelete() + { + $table = $this->wrapTable($this->from); + + $where = is_array($this->wheres) ? $this->compileWheres($this) : ''; + + $sql = trim("DELETE FROM $table ".$where); + + if (isset($this->limit)) { + $sql .= ' '.$this->compileLimit($this, $this->limit); + } + + return rtrim($sql); + } + + /** + * Compile a truncate table statement into SQL. + * + * @param \Database\Query $query + * @return array + */ + public function compileTruncate() + { + return array('TRUNCATE ' .$this->wrapTable($this->from) => array()); + } + + /** + * Wrap a table in keyword identifiers. + * + * @param string $table + * @return string + */ + public function wrapTable($table) + { + return $this->wrap($this->db->getTablePrefix() .$table); + } + + /** + * Wrap a value in keyword identifiers. + * + * @param string $value + * @return string + */ + public function wrap($value) + { + if (strpos(strtolower($value), ' as ') !== false) { + $segments = explode(' ', $value); + + return $this->wrap($segments[0]) .' AS ' .$this->wrap($segments[2]); + } + + $wrapped = array(); + + $segments = explode('.', $value); + + foreach ($segments as $key => $segment) { + if ($key == 0 && count($segments) > 1) { + $wrapped[] = $this->wrapTable($segment); + } else { + $wrapped[] = $this->wrapValue($segment); + } + } + + return implode('.', $wrapped); + } + + /** + * Wrap a single string in keyword identifiers. + * + * @param string $value + * @return string + */ + protected function wrapValue($value) + { + return ($value !== '*') ? sprintf($this->wrapper, $value) : $value; + } + + /** + * Concatenate an array of segments, removing empties. + * + * @param array $segments + * @return string + */ + protected function concatenate($segments) + { + return implode(' ', array_filter($segments, function($value) { + return (string) ($value !== ''); + })); + } + + /** + * Remove the leading boolean from a statement. + * + * @param string $value + * @return string + */ + protected function removeLeadingBoolean($value) + { + return preg_replace('/AND |OR /', '', $value, 1); + } + + /** + * Create query parameter place-holders for an array. + * + * @param array $values + * @return string + */ + public function parameterize(array $values) + { + return implode(', ', array_map(array($this, 'parameter'), $values)); + } + + /** + * Get the appropriate query parameter place-holder for a value. + * + * @param mixed $value + * @return string + */ + public function parameter($value) + { + return '?'; + } + + /** + * Convert an array of column names into a delimited string. + * + * @param array $columns + * @return string + */ + public function columnize(array $columns) + { + return implode(', ', array_map(array($this, 'wrap'), $columns)); + } +} diff --git a/system/functions.php b/system/functions.php index 2fa0487bea..81dc045c7b 100644 --- a/system/functions.php +++ b/system/functions.php @@ -67,6 +67,16 @@ function str_ends_with($haystack, $needle) return (($needle === '') || (substr($haystack, - strlen($needle)) === $needle)); } +/** + * Class name helper + * @param string $className + * @return string + */ +function class_basename($className) +{ + return basename(str_replace('\\', '/', $className)); +} + /** * Determine if the given object has a toString method. *