diff --git a/README.md b/README.md index 82ca0ed..0b10082 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +#Active repository forked from usmanhalalit + # Pixie Query Builder [![Build Status](https://travis-ci.org/usmanhalalit/pixie.png?branch=master)](https://travis-ci.org/usmanhalalit/pixie) A lightweight, expressive, framework agnostic query builder for PHP it can also be referred as a Database Abstraction Layer. Pixie supports MySQL, SQLite and PostgreSQL and it takes care of query sanitization, table prefixing and many other things with a unified API. At least PHP 5.3 is required. @@ -8,6 +10,7 @@ It has some advanced features like: - Sub Queries - Nested Queries - Multiple Database Connections. + - Added support for table creation/mutation The syntax is quite similar to Laravel's query builder. diff --git a/src/Pixie/EventHandler.php b/src/Pixie/EventHandler.php index 5d202ad..3ec48d8 100644 --- a/src/Pixie/EventHandler.php +++ b/src/Pixie/EventHandler.php @@ -67,7 +67,7 @@ public function removeEvent($event, $table = ':any') * @param $event * @return mixed */ - public function fireEvents($queryBuilder, $event) + public function fireEvents($queryBuilder, $event, &$args = array()) { $statements = $queryBuilder->getStatements(); $tables = isset($statements['tables']) ? $statements['tables'] : array(); @@ -77,22 +77,21 @@ public function fireEvents($queryBuilder, $event) array_unshift($tables, ':any'); // Fire all events + $counter = 0; foreach ($tables as $table) { // Fire before events for :any table if ($action = $this->getEvent($event, $table)) { // Make an event id, with event type and table - $eventId = $event . $table; - - // Fire event - $handlerParams = func_get_args(); - unset($handlerParams[1]); // we do not need $event - // Add to fired list - $this->firedEvents[] = $eventId; - $result = call_user_func_array($action, $handlerParams); - if (!is_null($result)) { - return $result; - }; + $eventId = $event . $table; + $params = array($queryBuilder, $tables[$counter + 1], &$args); + // Add to fired list + $this->firedEvents[] = $eventId; + $result = call_user_func_array($action, $params); + if (!is_null($result)) { + return $result; + }; } + $counter++; } } } diff --git a/src/Pixie/QueryBuilder/Adapters/BaseAdapter.php b/src/Pixie/QueryBuilder/Adapters/BaseAdapter.php index ade1a1b..a0c34f5 100644 --- a/src/Pixie/QueryBuilder/Adapters/BaseAdapter.php +++ b/src/Pixie/QueryBuilder/Adapters/BaseAdapter.php @@ -306,6 +306,228 @@ public function delete($statements) return compact('sql', 'bindings'); } + + /* + * Create table and returns full query + * + * @param $statements + * + * @return array + * @throws Exception + */ + public function createTable($statements) + { + if (!isset($statements['tables'])) { + throw new Exception('No table specified', 3); + } + + if (!isset($statements['columns'])) { + throw new Exception('No columns specified', 3); + } + + $table = end($statements['tables']); + + $criteria = "("; + for($i = 0; $i < count($statements["columns"]);$i++) + { + $statement = $statements["columns"][$i]; + $values = $this->createDynamicCollectionOfValuesForStatement($statement); + $criteria .= $this->createCriteriaForStatement($values, "name datatype(size) constraint", ($i < (count($statements["columns"]) - 1))); + } + $criteria .= ")"; + + $sqlArray = array('CREATE TABLE', $this->wrapSanitizer($table), $criteria); + $sql = $this->concatenateQuery($sqlArray, ' ', false); + $bindings = array(); + return compact('sql', 'bindings'); + } + + /* + * using the str_ireplace make it possible to replace names by array keys which are the same as in the format + */ + private function createCriteriaForStatement($values, $format, $addCommaToEnd) + { + if (empty($values)) { + throw new Exception('No values specified', 3); + } + + $format = $this->addCommaIfNeeded($format, $addCommaToEnd); + return str_ireplace(array_keys($values), $values, $format); + } + + /* + * Adding comma to format when its not last item + */ + private function addCommaIfNeeded($format, $addCommaToEnd) + { + if($addCommaToEnd) { + return $format . ", "; + } + return $format; + } + + /* + * Alter table and returns full query + * @param $statements + * + * @return array + * @throws Exception + */ + public function alterTable($statements) + { + $totalColumns = count($statements["columns"]); + + if (!isset($statements['tables'])) { + throw new Exception('No table specified', 3); + } + + if (!isset($statements['columns']) || $totalColumns <= 0) { + throw new Exception('No columns specified', 3); + } + + $table = end($statements['tables']); + $criteria = ""; + if($totalColumns > 1) { + //Batch + for($i = 0; $i < $totalColumns;$i++) + { + $statement = $statements["columns"][$i]; + $criteria .= $this->getAlterTableColumnCriteriaForBatchStatement($statement, ($i < ($totalColumns - 1))); + } + } else { + $statement = $statements["columns"][0]; + //Single + $criteria = $this->getAlterTableColumnCriteriaForSingleStatement($statement); + } + + $sqlArray = array('ALTER TABLE', $this->wrapSanitizer($table), $criteria); + $sql = $this->concatenateQuery($sqlArray, ' ', false); + $bindings = array(); + return compact('sql', 'bindings'); + } + + /* + * Returns criteria when add/modify one column + */ + private function getAlterTableColumnCriteriaForSingleStatement($statement) + { + switch(strtolower($statement['action'])) + { + case "add": + $values = $this->createDynamicCollectionOfValuesForStatement($statement); + return $this->createCriteriaForStatement($values, "ADD COLUMN name datatype(size) constraint", false); + case "modify": + $values = $this->createDynamicCollectionOfValuesForStatement($statement); + return $this->createCriteriaForStatement($values, "MODIFY COLUMN name datatype(size) constraint", false); + case "rename": + $values = $this->createDynamicCollectionOfValuesForStatement($statement, array("old_name", "new_name")); + return $this->createCriteriaForStatement($values, "CHANGE old_name new_name datatype(size) constraint", false); + case "drop": + $values = $this->createDynamicCollectionOfValuesForStatement($statement); + return $this->createCriteriaForStatement($values, "DROP COLUMN name ", false); + default: + throw new Exception('No unknown action is query specified.', 3); + break; + } + } + + /* + * Returns criteria when multiple columns mutated in the table + */ + private function getAlterTableColumnCriteriaForBatchStatement($statement, $addCommaToEnd) + { + switch(strtolower($statement['action'])) + { + case "add": + $values = $this->createDynamicCollectionOfValuesForStatement($statement); + return $this->createCriteriaForStatement($values, "ADD name datatype(size) constraint", $addCommaToEnd); + case "modify": + $values = $this->createDynamicCollectionOfValuesForStatement($statement); + return $this->createCriteriaForStatement($values, "MODIFY COLUMN name datatype(size) constraint", $addCommaToEnd); + case "rename": + $values = $this->createDynamicCollectionOfValuesForStatement($statement, array("old_name", "new_name")); + return $this->createCriteriaForStatement($values, "CHANGE old_name new_name datatype(size) constraint", $addCommaToEnd); + case "drop": + $values = $this->createDynamicCollectionOfValuesForStatement($statement); + return $this->createCriteriaForStatement($values, "DROP name ", $addCommaToEnd); + default: + throw new Exception('No unknown action is query specified.', 3); + break; + } + } + + /* + * Creates an array where the values are transformed in the correct format, like uppercasing or sanitize + */ + private function createDynamicCollectionOfValuesForStatement($statement, $sanitizeNames = array("name"), $uppercaseNames = array("datatype", "constraint")) + { + $values = array(); + foreach($statement as $key => $value) + { + if($key == "action") + continue; + + if(is_array($value)) { + $value = implode(" ", $value); + } + + if(in_array($key, $sanitizeNames)) { + $values[$key] = $this->wrapSanitizer($value); + } elseif(in_array($key, $uppercaseNames)) { + $values[$key] = strtoupper($value); + } elseif(is_numeric($value)) { + $values[$key] = (int)$value; + } else { + $values[$key] = $value; + } + } + return $values; + } + + /* + * Drops a table from database + * @param $statements + * + * @return array + * @throws Exception + */ + public function dropTable($statements) + { + if (!isset($statements['tables'])) { + throw new Exception('No table specified', 3); + } + + $table = end($statements['tables']); + + $sqlArray = array('DROP TABLE', $this->wrapSanitizer($table)); + $sql = $this->concatenateQuery($sqlArray, ' ', false); + $bindings = array(); + + return compact('sql', 'bindings'); + } + + /* + * Renames a table in the database + * @param $statements + * + * @return array + * @throws Exception + */ + public function renameTable($statements) + { + if (!isset($statements['tables'])) { + throw new Exception('No table specified', 3); + } + + $table_new = end($statements['tables']); + $table_old = prev($statements['tables']); + + $sqlArray = array('RENAME TABLE', $this->wrapSanitizer($table_old), "TO", $this->wrapSanitizer($table_new)); + $sql = $this->concatenateQuery($sqlArray, ' ', false); + $bindings = array(); + + return compact('sql', 'bindings'); + } /** * Array concatenating method, like implode. @@ -534,4 +756,4 @@ protected function buildJoin($statements) return $sql; } -} +} \ No newline at end of file diff --git a/src/Pixie/QueryBuilder/QueryBuilderHandler.php b/src/Pixie/QueryBuilder/QueryBuilderHandler.php index 134fba0..8a85367 100644 --- a/src/Pixie/QueryBuilder/QueryBuilderHandler.php +++ b/src/Pixie/QueryBuilder/QueryBuilderHandler.php @@ -178,7 +178,8 @@ public function get() $result = call_user_func_array(array($this->pdoStatement, 'fetchAll'), $this->fetchParameters); $executionTime += microtime(true) - $start; $this->pdoStatement = null; - $this->fireEvents('after-select', $result, $executionTime); + $optionalInfo = array("result" => &$result, "executionTime" => $executionTime); + $this->fireEvents('after-select', $optionalInfo); return $result; } @@ -276,7 +277,7 @@ protected function aggregate($type) */ public function getQuery($type = 'select', $dataToBePassed = array()) { - $allowedTypes = array('select', 'insert', 'insertignore', 'replace', 'delete', 'update', 'criteriaonly'); + $allowedTypes = array('select', 'insert', 'insertignore', 'replace', 'delete', 'update', 'criteriaonly', "createtable", "altertable", "droptable", "renametable"); if (!in_array(strtolower($type), $allowedTypes)) { throw new Exception($type . ' is not a known type.', 2); } @@ -312,7 +313,8 @@ public function subQuery(QueryBuilderHandler $queryBuilder, $alias = null) */ private function doInsert($data, $type) { - $eventResult = $this->fireEvents('before-insert'); + $optionalInfo = array("data" => &$data); + $eventResult = $this->fireEvents('before-insert', $optionalInfo); if (!is_null($eventResult)) { return $eventResult; } @@ -341,10 +343,205 @@ private function doInsert($data, $type) } } - $this->fireEvents('after-insert', $return, $executionTime); + $optionalInfo = array("result" => &$result, "executionTime" => $executionTime); + $this->fireEvents('after-insert', $optionalInfo); return $return; } + + /** + * Creates a new table + * Example: QB::table("new_table") + * ->addColumn("ïd", "int", 11, array("NOT NULL", "PRIMARY KEY")) + * ->createTable(); + * + * @return array|string + */ + public function createTable() + { + return $this->tableMutation("createTable"); + } + + /** + * Alter an existing table + * Example: QB::table("existing_table") + * ->addColumn("ïd", "int", 11, array("NOT NULL", "PRIMARY KEY")) + * ->alterTable(); + * Options to use: + * Add columns + * Modifiy columns + * Rename columns + * Drop columns + * + * @return array|string + */ + public function alterTable() + { + return $this->tableMutation("alterTable"); + } + + /** + * Drops an existing table + * Example: QB::table("existing_table") + * ->dropTable(); + * + * @return array|string + */ + public function dropTable() + { + return $this->tableMutation("dropTable"); + } + + /** + * Drops an existing table + * Example: QB::renameTable("old_name", "new_name"); + * + * @param $old_name + * @param $new_name + * + * @return array|string + */ + public function renameTable($old_name, $new_name) + { + $this->addStatement('tables', array($old_name, $new_name)); + return $this->tableMutation("renameTable"); + } + + /* + * Handles all different table mutations + */ + private function tableMutation($action) + { + $eventResult = $this->fireEvents('before-' . $action); + if (!is_null($eventResult)) { + return $eventResult; + } + + $queryObject = $this->getQuery($action, $this->statements['columns']); + + list($response, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings()); + + $optionalInfo = array("response" => &$response, "executionTime" => $executionTime); + $this->fireEvents('after-' . $action, $optionalInfo); + + return $response; + } + + /** + * Adding an column + * Possible for: + * Creating new tables + * Alternating the table + * + * Example: QB::table("existing_table") + * ->addColumn("ïd", "int", 11, array("NOT NULL", "PRIMARY KEY")) + * ->alterTable(); + * + * @param $name - The name of the column + * @param $datatype - Type of the column (varchar/int/bigint etc.) + * @param $size - The size of the column + * @param $constraint - An array of constraints. Options supported: 'not null', 'null', 'auto_increment', 'primary key' + * + * @return querybuilder + */ + public function addColumn($name, $datatype, $size, $constraint = array()) + { + $this->validateConstraintIsSupported($constraint); + + $action = "add"; + $this->statements['columns'][] = compact('action', 'name', 'datatype', 'size', 'constraint'); + + return $this; + } + + /** + * Modify an column + * Possible for: + * Alternating the table + * + * Example: QB::table("existing_table") + * ->modifyColumn("ïd", "int", 11, array("NOT NULL", "PRIMARY KEY")) + * ->alterTable(); + * + * @param $name - The name of the column + * @param $datatype - Type of the column (varchar/int/bigint etc.) + * @param $size - The size of the column + * @param $constraint - An array of constraints. Options supported: 'not null', 'null', 'auto_increment', 'primary key' + * + * @return querybuilder + */ + public function modifyColumn($name, $datatype, $size, $constraint = array()) + { + $this->validateConstraintIsSupported($constraint); + + $action = "modify"; + $this->statements['columns'][] = compact('action', 'name', 'datatype', 'size', 'constraint'); + + return $this; + } + + /** + * Renames an column + * Possible for: + * Alternating the table + * + * Example: QB::table("existing_table") + * ->renameColumn("an_id", "ïd", "int", 11, array("NOT NULL", "PRIMARY KEY")) + * ->alterTable(); + * + * @param $old_name - The current name of the column + * @param $new_name - The new name + * @param $datatype - Type of the column (varchar/int/bigint etc.) + * @param $size - The size of the column + * @param $constraint - An array of constraints. Options supported: 'not null', 'null', 'auto_increment', 'primary key' + * + * @return querybuilder + */ + public function renameColumn($old_name, $new_name, $datatype, $size, $constraint = array()) + { + $this->validateConstraintIsSupported($constraint); + + $action = "rename"; + $this->statements['columns'][] = compact('action', 'old_name', 'new_name', 'datatype', 'size', 'constraint'); + + return $this; + } + + /** + * Drops an column + * Possible for: + * Alternating the table + * + * Example: QB::table("existing_table") + * ->dropColumn("ïd") + * ->alterTable(); + * + * @param $name - The name of the column + * + * @return querybuilder + */ + public function DropColumn($name) + { + $action = "drop"; + $this->statements['columns'][] = compact('action', 'name'); + + return $this; + } + + /* + * Validates the given constraints before processing them + */ + private function validateConstraintIsSupported(&$constraint) + { + $allowedTypes = array('not null', 'null', 'auto_increment', 'primary key'); + if(!is_array($constraint)) { + $constraint = array($constraint); + } + + if(array_diff(array_map('strtolower', $constraint), $allowedTypes)) { + throw new Exception(implode(" ", $constraint) . ' is not a known type.', 2); + } + } /** * @param $data @@ -383,7 +580,8 @@ public function replace($data) */ public function update($data) { - $eventResult = $this->fireEvents('before-update'); + $optionalInfo = array("data" => &$data); + $eventResult = $this->fireEvents('before-update', $optionalInfo); if (!is_null($eventResult)) { return $eventResult; } @@ -391,7 +589,8 @@ public function update($data) $queryObject = $this->getQuery('update', $data); list($response, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings()); - $this->fireEvents('after-update', $queryObject, $executionTime); + $optionalInfo = array("result" => &$result, "executionTime" => $executionTime); + $this->fireEvents('after-update', $optionalInfo); return $response; } @@ -434,7 +633,8 @@ public function delete() $queryObject = $this->getQuery('delete'); list($response, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings()); - $this->fireEvents('after-delete', $queryObject, $executionTime); + $optionalInfo = array("queryObject" => &$queryObject, "executionTime" => $executionTime); + $this->fireEvents('after-delete', $optionalInfo); return $response; } @@ -1047,11 +1247,10 @@ public function removeEvent($event, $table = ':any') * @param $event * @return mixed */ - public function fireEvents($event) + public function fireEvents($event, &$args = array()) { - $params = func_get_args(); - array_unshift($params, $this); - return call_user_func_array(array($this->connection->getEventHandler(), 'fireEvents'), $params); + $parameters = array($this, $event, &$args); + return call_user_func_array(array($this->connection->getEventHandler(), 'fireEvents'), $parameters); } /** @@ -1061,4 +1260,4 @@ public function getStatements() { return $this->statements; } -} +} \ No newline at end of file diff --git a/tests/Pixie/QueryBuilderBehaviorTest.php b/tests/Pixie/QueryBuilderBehaviorTest.php index 40030d9..3c8b510 100644 --- a/tests/Pixie/QueryBuilderBehaviorTest.php +++ b/tests/Pixie/QueryBuilderBehaviorTest.php @@ -302,4 +302,99 @@ public function testIsPossibleToUseSubqueryInWhereNotClause() $query->getQuery()->getRawSql() ); } -} + + public function testShouldMakeDataTypeUppercaseInQuery() + { + $query = $this->builder->table('new_table') + ->AddColumn("id", "int", 11); + + $this->assertEquals("CREATE TABLE `cb_new_table` (`id` INT(11) )", $query->getQuery("createTable")->getRawSql()); + } + + public function testCreateTableWithMultipleColumnTypesQuery() + { + $query = $this->builder->table('new_table') + ->AddColumn("id", "int", 11, array("NOT NULL", "PRIMARY KEY")) + ->AddColumn("column_1", "varchar", 255, array("NOT NULL")) + ->AddColumn("column_2", "bigint", 2); + + $this->assertEquals("CREATE TABLE `cb_new_table` (`id` INT(11) NOT NULL PRIMARY KEY, `column_1` VARCHAR(255) NOT NULL, `column_2` BIGINT(2) )", $query->getQuery("createTable")->getRawSql()); + } + + public function testCreateTableWithOneColumnQuery() + { + $query = $this->builder->table('new_table') + ->AddColumn("id", "int", 11, array("NOT NULL", "PRIMARY KEY")); + + $this->assertEquals("CREATE TABLE `cb_new_table` (`id` INT(11) NOT NULL PRIMARY KEY)", $query->getQuery("createTable")->getRawSql()); + } + + public function testAlterTableWithAddMultipleColumnTypesQuery() + { + $query = $this->builder->table('new_table') + ->AddColumn("id", "int", 11, array("NOT NULL", "PRIMARY KEY")) + ->AddColumn("column_1", "varchar", 255, array("NOT NULL")) + ->AddColumn("column_2", "bigint", 2); + + $this->assertEquals("ALTER TABLE `cb_new_table` ADD `id` INT(11) NOT NULL PRIMARY KEY, ADD `column_1` VARCHAR(255) NOT NULL, ADD `column_2` BIGINT(2)" + , $query->getQuery("alterTable")->getRawSql()); + } + + public function testAlterTableWithAddOneColumnQuery() + { + $query = $this->builder->table('new_table') + ->AddColumn("id", "int", 11, array("NOT NULL", "PRIMARY KEY")); + + $this->assertEquals("ALTER TABLE `cb_new_table` ADD COLUMN `id` INT(11) NOT NULL PRIMARY KEY", $query->getQuery("alterTable")->getRawSql()); + } + + public function testAlterTableWithModifiyMultipleColumnTypesQuery() + { + $query = $this->builder->table('new_table') + ->ModifyColumn("id", "int", 11, array("NOT NULL", "PRIMARY KEY")) + ->ModifyColumn("column_1", "varchar", 255, array("NOT NULL")) + ->ModifyColumn("column_2", "bigint", 2); + + $this->assertEquals("ALTER TABLE `cb_new_table` MODIFY COLUMN `id` INT(11) NOT NULL PRIMARY KEY, MODIFY COLUMN `column_1` VARCHAR(255) NOT NULL, MODIFY COLUMN `column_2` BIGINT(2)", $query->getQuery("alterTable")->getRawSql()); + } + + public function testAlterTableWithModifiyOneColumnQuery() + { + $query = $this->builder->table('new_table') + ->ModifyColumn("id", "int", 11, array("NOT NULL", "PRIMARY KEY")); + + $this->assertEquals("ALTER TABLE `cb_new_table` MODIFY COLUMN `id` INT(11) NOT NULL PRIMARY KEY", $query->getQuery("alterTable")->getRawSql()); + } + + public function testAlterTableWithDropMultipleColumnTypesQuery() + { + $query = $this->builder->table('new_table') + ->DropColumn("id") + ->DropColumn("column_1") + ->DropColumn("column_2"); + + $this->assertEquals("ALTER TABLE `cb_new_table` DROP `id` , DROP `column_1` , DROP `column_2`", $query->getQuery("alterTable")->getRawSql()); + } + + public function testAlterTableWithDropOneColumnQuery() + { + $query = $this->builder->table('new_table') + ->DropColumn("id"); + + $this->assertEquals("ALTER TABLE `cb_new_table` DROP COLUMN `id`", $query->getQuery("alterTable")->getRawSql()); + } + + public function testDropTableQuery() + { + $query = $this->builder->table('new_table'); + + $this->assertEquals("DROP TABLE `cb_new_table`", $query->getQuery("dropTable")->getRawSql()); + } + + public function testRenameTableQuery() + { + $query = $this->builder->table(array('old_table', 'new_table')); + + $this->assertEquals("RENAME TABLE `cb_old_table` TO `cb_new_table`", $query->getQuery("renameTable")->getRawSql()); + } +} \ No newline at end of file