Advertisement
Guest User

Untitled

a guest
Mar 7th, 2018
75
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 29.78 KB | None | 0 0
  1. <?php/** * This stores the base class for all database tables. * * @package Kingga\Data\Database */namespace Kingga\Data\Database;// Include the configuration file.$conf_file = dirname(dirname(dirname(__FILE__))) . '/conf.php';if (file_exists($conf_file)) require_once($conf_file);else throw new \Exception("Cannot find the configuration file.", 1);/** @var array MySQL data type equivalents to PHP */const DATA_TYPES = array( 'char' => 'string', 'varchar' => 'string', 'tinytext' => 'string', 'text' => 'string', 'mediumtext' => 'string', 'longtext' => 'string', 'binary' => 'string', 'varbinary' => 'string', 'bit' => 'int', 'tinyint' => 'int', 'smallint' => 'int', 'mediumint' => 'int', 'int' => 'int', 'integer' => 'int', 'bigint' => 'int', 'decimal' => 'float', 'dec' => 'float', 'numeric' => 'float', 'fixed' => 'float', 'float' => 'float', 'double' => 'float', 'real' => 'float', 'bool' => 'bool', 'boolean' => 'bool', 'date' => 'string', 'datetime' => 'string', 'timestamp' => 'string', 'time' => 'string', 'year' => 'string', 'tinyblob' => 'string', 'blob' => 'string', 'mediumblob' => 'string', 'longtex' => 'string');const DATA_TYPE_EXTRAS = array( 'unsigned', 'zerofill', 'character set ?', 'collate ?');// NOTE Because I cannot bind comparison operators I have created a whilelist// array which can be used as comparison operators./** @var array A whitelist array of allowed conditional operators. This is only for prepared statements. */const CONDITIONAL_OPERATOR_WHITELIST = array( '=', '<=>', '>', '>=', 'IS', 'IS NOT', 'IS NOT NULL', 'IS NULL', '<', '<=', 'LIKE', '!=', '<>', 'NOT LIKE');class Column { /** @var string The name of the column. */ private $name; /** @var string The type of the column using MySQL data types. */ private $mysql_type; /** @var string The type of the column using PHP data types. */ private $php_type; /** @var int The size of the data. */ private $size; /** @var int Extra info on the data type. E.g. unsigned. */ private $type_extra; /** @var bool Can the value be NULL? */ private $null; /** @var string What is the key for the column. Currently not set up to support foreign key references. */ private $key; /** @var mixed The default value of the column. */ private $default; /** @var string Any extra data. */ private $extra; /** * The constructor for a tables column structure. * @param string $name The name of the column. * @param string $type The type of column. E.g. varchar, int, etc. * @param int $size (optional) The size of the column. * @param string $type_extra (optional) Extra data which goes with the type. E.g. unsigned. * @param boolean $allow_nulls (optional) Can the data for this column be NULL? * @param string $key (optional) Does the column have a key? This is currently just a placeholder for the future. * @param mixed $default (optional) The default value of the column. * @param string $extra (optional) Any extra information on the column. */ public function __construct( string $name, string $type, int $size = null, string $type_extra = null, bool $allow_nulls = true, string $key = null, $default = null, string $extra = null ) { // Validate parameters. if (strlen($name) > 64) throw new \Exception("The name of the column cannot be longer than 64 characters.", 1); if (!isset(DATA_TYPES[$type])) throw new \Exception("The data type doesn't exist.", 1); // Set properties. $this->name = $name; $this->mysql_type = $type; try { $this->php_type = DATA_TYPES[$type]; } catch (Exception $e) { throw new \Exception('The data type is invalid.', 1); } $this->size = $size; $this->type_extra = $type_extra; $this->null = $allow_nulls; $this->key = $key; $this->default = $default; $this->extra = $extra; } /** * The getter/setter for $this->name. * @param string $name The new value for $this->name. * @return string The current value of $this->name. */ public function Name(string $name = null): string { if ($name !== null) $this->name = $name; return $this->name; } /** * The getter/setter for $this->mysql_type. * @param string $type The new value for $this->mysql_type. * @return string The current value of $this->mysql_type. */ public function MySQLType(string $type = null): string { if ($type !== null) $this->mysql_type = $type; return $this->mysql_type; } /** * The getter/setter for $this->php_type. * @param string $type The new value for $this->php_type. * @return string The current value of $this->php_type. */ public function PHPType(string $type = null): string { if ($type !== null) $this->php_type = $type; return $this->php_type; } /** * The getter/setter for $this->size. * @param int $size The new value for $this->size. * @return int The current value of $this->size. */ public function Size(int $size = null): int { if ($size !== null) $this->size = $size; return $this->size; } /** * The getter/setter for $this->type_extra. * @param string $ext The new value for $this->type_extra. * @return string The current value of $this->type_extra. */ public function TypeExtra(string $ext = null): string { if ($ext !== null) $this->type_extra = $ext; return $this->type_extra; } /** * The getter/setter for $this->null. * @param bool $null The new value for $this->null. * @return bool The current value of $this->null. */ public function Null(bool $null = null): bool { if ($null !== null) $this->null = $null; return $this->null; } /** * The getter/setter for $this->key. * @param string $null The new value for $this->key. * @return string The current value of $this->key. */ public function Key(string $key = null): string { if ($key !== null) $this->key = $key; return $this->key; } /** * The getter/setter for $this->default. * @param string $default The new value for $this->default. * @return string The current value of $this->default. */ public function Default(string $default = null): string { if ($default !== null) $this->default = $default; return $this->default; } /** * The getter/setter for $this->extra. * @param string $extra The new value for $this->extra. * @return string The current value of $this->extra. */ public function Extra(string $extra = null): string { if ($extra !== null) $this->extra = $extra; return $this->extra; } /** * Check if a value is the same type as the column. * @param string $php_type The type to check against. * @param string $val The value to check. * @return bool Is it the same type? */ protected function validate_value(string $php_type, string $val): bool { if (gettype($php_type) === gettype($val)) return true; else return false; }}class Table { /** @var \PDO This is the handle to the database connection. */ private $conn; /** @var string The name of the table. */ private $name; /** @var array The columns of the table. */ private $columns; /** * The cached set of rows in the table. The data inside of this variable will * look like this: * <pre><code>array( * // Columns horozontal, rows vertical. * array(1, 'John', 'Doe', 21, 'M'), // First row. * array(2, 'Jane', 'Doe', 22, 'F'), // Second row. * ) * </code></pre> * @var array */ private $rows; // Table information below. /** @var string The engine that the table is running on. */ private $engine; /** @var string The tables collation. */ private $collation; /** @var string The date and time the table was created. */ private $created; /** @var string Any comments added to the table. */ private $comment; /** * This is the constructor for the Table. Inside of this function, the PDO * object is initialized and the variable types are set. * The following constants must be set before initiailzing this class. * <ul> * <li>DB_HOST = localhost/127.0.0.1/etc. This can also include the port (append :port_number to the end)</li> * <li>DB_NAME = The name of the database</li> * <li>DB_USER = The username to connect to the database with</li> * <li>DB_PASSWORD = The password for the user</li> * </ul> * @param string $name The name of the table. * @throws Exception If the table doesn't exist. */ public function __construct(string $name) { // Create the DSN. $host_port = explode(':', DB_HOST); $port_str = (isset($host_port[1])) ? 'port='.$host_port[1] : ''; $dsn = 'mysql:host='.$host_port[0].';dbname='.DB_NAME.';'.$port_str; // Create the PDO options. $opt = [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_EMULATE_PREPARES => false, \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_OBJ ]; // Initialize PDO. $this->conn = new \PDO($dsn, DB_USER, DB_PASS, $opt); // Set up default values. $this->columns = array(); $this->rows = array(); // Check if table with $name exists and if it does add it, otherwise // throw an exception. $this->name = $name; try { $this->table_info(); } catch (Exception $e) { $this->name = null; throw new \Exception('The table doesn\'t exist.', 1); } try { $this->describe_table(); } catch (Exception $e) { $this->name = null; throw new \Exception('The table doesn\'t exist.', 1); } } /** * This function gets data from a table and returns the data. * * This function gets data from this table and returns it. You can use * options to refine your query and return a smaller result set. Some * options that can be set include: * <ul> * <li>(array) columns - must be from $this->columns.</li> * <li>(array) where - must be in the following format.</li> * <uL> * <li><code>array('LeftValue', 'ComparisonOperator', 'RightValue')</code></li> * </ul> * <li>(array) group - must be in the following format.</li> * <ul> * <li><code>array('ColumnName', 'ASC|DESC')</code></li> * </ul> * <li>(array) order - must be in the following format.</li> * <ul> * <li><code>array('ColumnName', 'ASC|DESC')</code></li> * </ul> * <li>(array) limit - an array with 1 or 2 values (just like the MySQL docs).</li> * </ul> * Code example below: * <pre><code>$results = $table->Get( * array( * 'columns' => array('col1', 'col2'), * 'where' => array( * array('col3', '=', 'col4'), * array('col4', 'LIKE', 'King%') * ), * 'group' => array( * array('col4', 'ASC') * ), * 'order' => array( * array('col3', 'DESC') * ), * 'limit' => array(5) * ) * ); * </code></pre> * @param array $opt The options for the query. NULL if you want to return everything. * @param boolean $refresh If the current $rows property isn't NULL do you want to refresh the current cached data. * @return array The result set from the database. * @throws \PDOException If there was a failure with the database. */ public function Get(array $opt = null, bool $refresh = true): array { $info = $this->build_select_query($opt); // Run the statement (Don't catch the PDOException, let the user do it). $stmt = $this->conn->prepare($info['Query']); // Loop through the binds and add them. foreach ($info['Binds'] as $p => $v) { $this->bindParam($stmt, ":$p", $v); } $stmt->execute(); // Get the results. $results = $stmt->fetchAll(); $stmt->closeCursor(); return $results; } /** * Check if a table exists in the database. * @return bool Was the table found? */ protected function table_exists(): bool { try { // Try to get the table. $q = " SELECT COUNT(*) AS 'count' FROM information_schema.tables WHERE table_schema = :dbname AND table_name = :table LIMIT 1; "; $stmt = $this->conn->prepare($q); $dbname = DB_NAME; $stmt->bindParam(':dbname', $dbname, \PDO::PARAM_STR, 64); $stmt->bindParam(':table', $this->name, \PDO::PARAM_STR, 64); $result = $stmt->execute(); } catch (\PDOException $e) { // The table was not found. return false; } // Fetch results. $result = $stmt->fetchAll(); $stmt->closeCursor(); if ($result[0]->count) { // Table exists. return true; } else { // No table. return false; } } protected function table_info() { try { // Try to fetch the tables inforamtion. $q = " SELECT * FROM information_schema.tables WHERE table_schema = :dbname AND table_name = :table LIMIT 1; "; $stmt = $this->conn->prepare($q); $dbname = DB_NAME; $stmt->bindParam(':dbname', $dbname, \PDO::PARAM_STR, 64); $stmt->bindParam(':table', $this->name, \PDO::PARAM_STR, 64); $stmt->execute(); } catch (\PDOException $e) { throw new \Exception("The database failed to get the tables information.", 1); } // Get the tables information. $table_info = $stmt->fetchAll(); $stmt->closeCursor(); // If the result set is empty throw error. if (empty($table_info)) throw new \Exception("The database failed to get the tables information.", 1); $ti = $table_info[0]; // Set the table information. $this->engine = $ti->ENGINE; $this->created = $ti->CREATE_TIME; $this->collation = $ti->TABLE_COLLATION; $this->comment = $ti->TABLE_COMMENT; } /** * Descibes this current table and stores the structure in the classes * properties. * @throws Exception When the database failed to get the tables structure. */ protected function describe_table() { try { // Try to describe the table, otherwise throw an error. $q = " SELECT * FROM information_schema.columns WHERE table_schema = :dbname AND table_name = :table ORDER BY ordinal_position "; $stmt = $this->conn->prepare($q); $dbname = DB_NAME; // So I can pass the value by reference. $stmt->bindParam(':dbname', $dbname, \PDO::PARAM_STR, 64); $stmt->bindParam(':table', $this->name, \PDO::PARAM_STR, 64); // Maximum table length is 64 chars. $stmt->execute(); } catch (\PDOException $e) { throw new \Exception("The database failed to get the tables structure.", 1); } // Get the results. $table_struct = $stmt->fetchAll();
  2. $stmt->closeCursor(); // Process the data and add the column. foreach ($table_struct as $c) { $name = $c->COLUMN_NAME; $default = $c->COLUMN_DEFAULT; $null = (strtolower($c->IS_NULLABLE) === 'yes') ? true : false; $dt = $c->DATA_TYPE; $size = $c->CHARACTER_MAXIMUM_LENGTH; $charset = $c->CHARACTER_SET_NAME; // NOTE Currently not used. $dt_ext = null; foreach (DATA_TYPE_EXTRAS as $ext) { // Loop through each extra data type and add them with a space. // If $ext contains a question mark, use it as a wildcard. if (strpos($ext, '?')) { // Deal with the wildcard. // As far as I know this wont happen but I'll leave it here // just in case it does. $tmp_ext = str_replace('?', '', $ext); preg_match("/$tmp_ext \b(\S.*?)\b/i", $c->COLUMN_TYPE, $m); } else { // No wildcard to worry about. $dt_ext .= (strpos($c->COLUMN_TYPE, $ext)) ? $ext . ' ' : ''; } } $key = $c->COLUMN_KEY; $ext = $c->EXTRA; $comment = $c->COLUMN_COMMENT; // NOTE Currently not used. // Push the new column onto the array. array_push($this->columns, new Column( $name, $dt, $size, trim($dt_ext), $null, $key, $default, $ext )); } } /** * Convert the MySQL data type into a PHP datatype. * @param string $t The type to convert. * @return string The converted type. * @throws Exception When the data type cannot be found. */ protected function mysql_type_to_php_type(string $t): string { if (isset(DATA_TYPES[$t])) return DATA_TYPES[$t]; throw new \Exception("The MySQL data type '$t' could not be found.", 1); } /** * This function builds a select query and runs it given a set of options. * These options include: * * <ul> * <li>(array) columns - must be from $this->columns.</li> * <li>(array) where - must be in the following format.</li> * <uL> * <li><code>array('LeftValue', 'ComparisonOperator', 'RightValue')</code></li> * </ul> * <li>(array) group - must be in the following format.</li> * <ul> * <li><code>array('ColumnName', 'ASC|DESC')</code></li> * </ul> * <li>(array) order - must be in the following format.</li> * <ul> * <li><code>array('ColumnName', 'ASC|DESC')</code></li> * </ul> * <li>(array) limit - an array with 1 or 2 values (just like the MySQL docs).</li> * </ul> * Code example below: * <pre><code>$results = $this->build_select_query( * array( * 'columns' => array('col1', 'col2'), * 'where' => array( * array('col3', '=', 'col4'), * array('col4', 'LIKE', 'King%') * ), * 'group' => array( * array('col4', 'ASC') * ), * 'order' => array( * array('col3', 'DESC') * ), * 'limit' => array(5) * ) * ); * </code></pre> * @param array $opt The options used to build the query. * @param array $info This parameter is a referenced array which gives back the prepare statement and the prepared statement. * @return array The result set from the query. * @throws Exception When some required information is missing. * @throws PDOException If there was an error with the database. */ protected function build_select_query(array $opt, array &$info = null): array { // TODO TestCase Columns array is not set. Result should be "SELECT * FROM :table " // TODO TestCase Columns array is set to array(). Result should be "SELECT * FROM :table " // TODO TestCase Three valid columns should be added. Result should be "SELECT col1, col2, col3 FROM :table " // TODO TestCase One valid column should be added. Result should be "SELECT col1 FROM :table " // TODO TestCase One invalid column should be added. Result should be "SELECT * FROM :table " // TODO TestCase One invalid and two valid column should be added. Result should be "SELECT col2, col3 FROM :table " // TODO TestCase Add an empty where array. Result should be "SELECT * FROM :table "; // TODO TestCase Add a simple where array but miss one of the three values. It should throw an Exception. // TODO TestCase Add a simple where array with an invalid comparison operator. It should throw an Exception. // TODO TestCase Add a single valid where array. Result should be "SELECT * FROM :table WHERE :col1 = 'Test' " // TODO TestCase Add two valid where arrays. Result should be "SELECT * FROM :table WHERE :col1 = 'Test' AND :col2 = 'Case' " // TODO TestCase Add three valid where arrays. Result should be "SELECT * FROM :table WHERE :col1 = 'Test' AND :col2 = 'Case' AND :col3 = 'Successful' " // TODO TestCase Add an invalid group by 2nd parameter (e.g. array('col1', 'invalid')). Expect Exception // TODO TestCase Add an invalid group by column name. Expect missing group by clause - "SELECT * FROM :table " // TODO TestCase Add a single group by without a second parameter. Result should be "SELECT * FROM :table GROUP BY :col1 ASC " // TODO TestCase Add a single group by with a second parameter. Result should be "SELECT * FROM :table GROUP BY :col1 DESC " // TODO TestCase Add an invalid order by 2nd parameter (e.g. array('col1', 'invalid')). Expect Exception // TODO TestCase Add an invalid order by column name. Expect missing order by clause - "SELECT * FROM :table " // TODO TestCase Add a single order by without a second parameter. Result should be "SELECT * FROM :table ORDER BY :col1 ASC " // TODO TestCase Add a single order by with a second parameter. Result should be "SELECT * FROM :table ORDER BY :col1 DESC " // TODO TestCase Test limit with nothing set. Should throw Exception. // TODO TestCase Test limit with only index 0 set. Result should be "SELECT * FROM :table LIMIT :limit1" // TODO TestCase Test limit with index 0 and 1 set. Result should be "SELECT * FROM :table LIMIT :limit1, :limit2" $q = "SELECT "; // Start of the select statement. $pBinds = array(); // An array of binds. E.g. :bind, :other_bind, etc. // Add some columns. If none are set use '*' (select all). if (isset($opt['columns']) && !empty($opt['columns'])) { $col_count = count($opt['columns']); $i = 0; foreach ($opt['columns'] as $col) { // Check if the column exists. foreach ($this->columns as $k => $c) { if ($c->Name() === $col) { // Add it to the query. // If it's not the last column add a ',', otherwise don't. // I can add them directly to the string as I know that the column exists. if ($i < $col_count - 1) $q .= "`$col`, "; else $q .= "`$col` FROM `" . $this->name . "` "; break; } } $i++; } } else $q .= "* FROM `" . $this->name . "` "; // I know the table exists as well since I check in the constructor. // Add the where clause. if (isset($opt['where']) && !empty($opt['where'])) { $where_count = count($opt['where']); $i = 0; foreach ($opt['where'] as $k => $w) { // 0 = Left, 1 = Op, 2 = Right ($w[0], $w[1], $w[2]) // Make sure everything is set. if (isset($w[0]) && isset($w[1]) && isset($w[2])) { // Make sure that the operator is valid. Otherwise throw an exception. $op = trim(strtoupper($w[1])); if (!in_array($op, CONDITIONAL_OPERATOR_WHITELIST)) throw new \Exception("The comparison operator is not allowed.", 1); // Check if the column exists. foreach ($this->columns as $k => $c) { if ($c->Name() === $col) { // If it is the first one, use WHERE. if ($i === 0) $q .= "WHERE `" . $w[0] . "` " . $op . " :" . $w[0] . " "; else $q .= "AND `" . $w[0] . "` " . $op . " :" . $w[0] . " "; break; } } // Add the where clause to the binds array. $pBinds[$w[0]] = $w[2]; } else { throw new \Exception('Some of the required information has not been set in the where clause.', 1); } $i++; } } // Add the group clause. if (isset($opt['group']) && !empty($opt['group'])) { $group_count = count($opt['group']); $i = 0; $q .= "GROUP BY "; foreach ($opt['group'] as $k => $g) { // Make sure the requried information isset. // $g[1] = ASC|DESC is not required. ASC by default. if (isset($g[0])) { // Check if the column exists. foreach ($this->columns as $c) { if ($c->Name() === $g[0]) { // Add it to the query. if (isset($g[1])) { $g1upper = strtoupper($g[1]); if ($g1upper !== 'ASC' && $g1upper !== 'DESC') throw new \Exception("The group by 2nd parameter is invalid.", 1); if ($i < $group_count - 1) $q .= "`" . $g[0] . "` $g1upper, "; else $q .= "`" . $g[0] . "` $g1upper "; } else { if ($i < $group_count - 1) $q .= "`" . $g[0] . "` ASC, "; else $q .= "`" . $g[0] . "` ASC "; } break; } } } $i++; } } // Add the order by clause. if (isset($opt['order']) && !empty($opt['order'])) { $order_count = count($opt['order']); $i = 0; $q .= "ORDER BY "; foreach ($opt['order'] as $k => $o) { // Make sure the required information isset. // $o[1] = ASC|DESC is not required. ASC by default. if (isset($o[0])) { // Check if the column exists. foreach ($this->columns as $c) { if ($c->Name() === $o[0]) { // Add it to the query. if (isset($o[1])) { $o1upper = strtoupper($o[1]); if ($o1upper !== 'ASC' && $o1upper !== 'DESC') throw new \Exception("The order by 2nd parameter is invalid.", 1); if ($i < $order_count - 1) $q .= ":" . $o[0] . " $o1upper, "; else $q .= "`" . $o[0] . "` $o1upper "; } else { if ($i < $order_count - 1) $q .= ":" . $o[0] . " ASC, "; else $q .= "`" . $o[0] . "` ASC "; } break; } } } $i++; } } // Add the limit. if (isset($opt['limit']) && !empty($opt['limit'])) { $q .= "LIMIT "; if (isset($opt['limit'][0])) { $q .= ":limit1"; $pBinds['limit1'] = $opt['limit'][0]; } else throw new \Exception('The limit hasn\'t been specified.', 1); if (isset($opt['limit'][1])) { $q .= ", :limit2"; $pBinds['limit2'] = $opt['limit'][1]; } } // Remove duplicate parameters and replace them with unique ones. $q_original = $q; $this->prepareMultiBind($q, $pBinds); // Set $info to display the two queries and the new binds. $info = array( 'Prev' => $q_original, 'Query' => $q, 'Binds' => $pBinds ); return $info; } /** * Change multiple of the same binds to unique names. For example, :col, :col * become :col_1, :col_2 and the array will look like this, * <pre><code>array( * 'col_1' => 'Column One Value', * 'col_2' => 'Column One Value' * ); * </code></pre> * @param string $sql The SQL to convert. * @param array $binds The parameters to convert. */ protected function prepareMultiBind(string &$sql, array &$binds = array()) { foreach ($binds as $bindID => $bindV) { // Match all parameters which are in the SQL statement. preg_match_all("/:$bindID\b/", $sql, $pm); // Count how many matches there are, if any. $pCount = (isset($pm[0])) ? count($pm[0]) : 0; if ($pCount > 1) { // If there are some matches loop through them and change their // names to $previous_$index. for ($i = 1; $i <= $pCount; $i++) $binds[$bindID.'_'.$i] = $bindV; // Unset the old value. unset($binds[$bindID]); // Look for the binds in the SQL and change it to the new value. $i = 0; $sql = preg_replace_callback("(:$bindID\b)", function ($m) use (&$i) { $i++; return sprintf("%s_%d", $m[0], $i); }, $sql, $limit = -1, $i); } } } protected function bindParam(\PDOStatement &$stmt, string $id, string $value, int $length = null) { $type = null; if (is_numeric($value) && !is_string($value)) $type = \PDO::PARAM_INT; else if (is_bool($value)) $type = \PDO::PARAM_BOOL; else if (is_string($value)) $type = \PDO::PARAM_STR; else if (is_null($value)) $type = \PDO::PARAM_NULL; else throw new \Exception('Unknown data type ' . gettype($value) . '!', 1); $stmt->bindParam($id, $value, $type, $length); }}?>
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement