Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00%
0 / 1
70.83%
17 / 24
CRAP
58.70%
135 / 230
AiMysql
0.00%
0 / 1
70.83%
17 / 24
675.94
58.33%
133 / 228
 __construct( $dbHost, $dbUser, $dbPass, $dbName )
100.00%
1 / 1
1
100.00%
5 / 5
 connect()
0.00%
0 / 1
4.84
62.50%
5 / 8
 query( $query, $file, $line )
0.00%
0 / 1
3.71
57.14%
4 / 7
 escapeString( $string )
100.00%
1 / 1
1
100.00%
1 / 1
 blockInjection( $string )
100.00%
1 / 1
1
100.00%
2 / 2
 fetchRow( $result )
100.00%
1 / 1
1
100.00%
1 / 1
 fetchArray( $result )
100.00%
1 / 1
2
100.00%
6 / 6
 fetchFieldObject( $result, $columnOffset )
100.00%
1 / 1
1
100.00%
1 / 1
 fieldName( $result, $columnOffset )
100.00%
1 / 1
1
100.00%
2 / 2
 numRows( $result )
100.00%
1 / 1
1
100.00%
1 / 1
 numFields( $result )
100.00%
1 / 1
1
100.00%
1 / 1
 fieldType( $result, $columnOffset )
100.00%
1 / 1
1
100.00%
2 / 2
 simpleFieldType( $result, $columnOffset )
0.00%
0 / 1
26.51
90.91%
30 / 33
 fixBoolean( $booleanValue )
100.00%
1 / 1
2
100.00%
4 / 4
 fixDbBoolean( $booleanValue )
100.00%
1 / 1
8
100.00%
12 / 12
 insertBlank( $tableName, $fieldName, $requiredFields = FALSE )
100.00%
1 / 1
4
100.00%
24 / 24
 beginTransaction( $file = false, $line = false )
100.00%
1 / 1
1
100.00%
1 / 1
 endTransaction( $file = false, $line = false )
100.00%
1 / 1
1
100.00%
1 / 1
 getCurrentTimestamp( $format = 'database' )
100.00%
1 / 1
3
100.00%
10 / 10
 getCurrentVersion()
0.00%
0 / 1
3.04
83.33%
10 / 12
 getUpgradeFiles( $fromDir )
0.00%
0 / 1
30
0.00%
0 / 16
 validateUpgradeFiles( $files )
0.00%
0 / 1
12
0.00%
0 / 14
 doUpgrade( $fromVersion, $direction )
0.00%
0 / 1
182
0.00%
0 / 54
 recordSchemaChange( $action, $versionNumber )
100.00%
1 / 1
3
100.00%
10 / 10
<?php
/**
This file is part of the APP(ideas) database abstraction project (AiDb).
Copyright 2013, APPideas
AiDb is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
AiDb is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with AiDb (in the 'resources' directory). If not, see
<http://www.gnu.org/licenses/>.
http://appideas.com/abstract-your-database-introduction
*/
require_once( "base/AiDb.php" );
require_once( "util/AiUtil.php" );
/**
* An implementation of the abstraction of the MySQL database engine
*
* @package Ai_DatabaseAbstraction
*/
class AiMysql implements AiDb
{
/**
* Database host name
* @var string
*/
protected $mDbHost;
/**
* Database username
* @var string
*/
protected $mDbUser;
/**
* Database password
* @var string
*/
protected $mDbPass;
/**
* Database schema name
* @var string
*/
protected $mDbName;
/**
* Resource created by the database connection
* @var resource
*/
public $mConnectionResource;
/**
* The name of the table that holds the schema version update history
* @var string
*/
protected $mVersionsTable = "schema_versions";
/**
* Class constructor.
*
* Setup parameters through AiCommon and access the database through that class' instance of this class
*
* @since Version 20120328
* @author Version 20120328, costmo
* @param string $dbHost The database host name
* @param string $dbUser The database username
* @param mixed $dbPass The database password
* @param string $dbName The name of the database
*/
public function __construct( $dbHost, $dbUser, $dbPass, $dbName )
{
$this->mDbHost = $dbHost;
$this->mDbUser = $dbUser;
$this->mDbPass = $dbPass;
$this->mDbName = $dbName;
} // Constructor
/**
* Connect to the database and set the member connection resource
*
* @return void
* @since Version 20120328
* @author Version 20120328, costmo
*/
public function connect()
{
try
{
$this->mConnectionResource = new mysqli( $this->mDbHost, $this->mDbUser, $this->mDbPass, $this->mDbName );
if( !$this->mConnectionResource || $this->mConnectionResource->connect_errno > 0 )
{
echo $exception->getMessage();
throw new Exception( "CONNECTION ERROR: Could not connect to the MySQL database. Check your connection parameters in base/Common.php" );
}
}
catch( Exception $exception )
{
// if we could not connect, halt execution and display an error
throw new Exception( $exception->getMessage() );
}
} // connect()
/**
* Perform a query of the database.
*
* This does not assume that strings have been escaped, so be sure to escape them.
*
* Use the PHP macros __FILE__ and __LINE__ for the 2nd and 3rd parameters.
*
* @return resource
* @since Version 20120328
* @author Version 20120328, costmo
* @param string $query The query to perform
* @param string $file The file making the query
* @param int $line The line number in the querying file
*/
public function query( $query, $file, $line )
{
// try a successful query
try
{
@$result = $this->mConnectionResource->query( $query );
if( !$result )
{
throw new Exception( "QUERY ERROR: In file " . $file . ", line number " . $line . ", MySQL said: " . $this->mConnectionResource->error . "\n\nThe query was: \n" . $query . "\n" );
}
// the return value is only hit on no exception
return $result;
}
catch( Exception $exception )
{
// show an error and halt execution. This is OK in Postgres - if we are in a transaction,
// a die before the END or COMMIT block causes an implicit ROLLBACK
throw new Exception( $exception->getMessage() );
}
} // query()
/**
* Escape a string in a manner that the chosen DBMS can handle
*
* For consistency with SQL standards, we won't use mConnectionResource->real_escape_string() even though it works for mysqli insertions
*
* @return string
* @since Version 20120328
* @author Version 20120328, costmo
* @param string $string The string to escape
*/
public function escapeString( $string )
{
return preg_replace( '/\'/', '\'\'', $string );
} // escapeString
/**
* Returns any part of a query before a semi-colon
*
* If there is an attempt of an injection attack, or if your string input legitimately has semicolons,
* this is likely to cause truncated input.
*
* If your input may contain semicolons, be sure that you trust the source before allowing it to perfom
* a query.
*
* @return string
* @since Version 20120328
* @author Version 20120328, costmo
* @param string $string The string to de-inject
*/
public function blockInjection( $string )
{
$split = preg_split( "/;/", $string );
return $split[0];
}
/**
* Fetch a row form the database
*
* @return array
* @since Version 20120328
* @author Version 20120328, costmo
* @param resource $result The conenction resource returned by a query
*/
public function fetchRow( $result )
{
return $result->fetch_row();
} // fetchRow()
/**
* Fetch a row form the database and return it as an indexed array and an associative array with field names as indexes
*
* For consistency, returned keys are cast to lower case
*
* @return array
* @since Version 20120328
* @author Version 20120328, costmo
* @param resource $result The conenction resource returned by a query
*/
public function fetchArray( $result )
{
$returnValue = $result->fetch_array();
// The final attempt at fetch_array returns false when all records are retrieved in a loop,
// but array_change_key_case doesn't like that
if( is_array( $returnValue ) )
{
// convert all keys to lower case
$returnValue = array_change_key_case( $returnValue );
}
return $returnValue;
} // fetchArray()
/**
* Gets an object of the field at the given offset for other methods to use
*
* @return object
* @since Version 20120328
* @author Version 20120328, costmo
* @param resource $result The conenction resource returned by a query
* @param int $columnOffset The offset of the requested column (0 is the first column)
*/
public function fetchFieldObject( $result, $columnOffset )
{
return $result->fetch_field_direct( $columnOffset );
} // fetchField()
/**
* Get the name of a field at the given offset
*
* For consistency, returned values are cast to lower case
*
* @return string
* @since Version 20120328
* @author Version 20120328, costmo
* @param resource $result The conenction resource returned by a query
* @param int $columnOffset The offset of the requested column (0 is the first column)
*/
public function fieldName( $result, $columnOffset )
{
$field = $this->fetchFieldObject( $result, $columnOffset );
return strtolower( $field->name );
} // fieldName()
/**
* The number of rows returned by a query
*
* @return int
* @since Version 20120328
* @author Version 20120328, costmo
* @param resource $result The conenction resource returned by a query
*/
public function numRows( $result )
{
return $result->num_rows;
} // numRows()
/**
* The number of fields returned by a query
*
* @return int
* @since Version 20120328
* @author Version 20120328, costmo
* @param resource $result The conenction resource returned by a query
*/
public function numFields( $result )
{
return $result->field_count;
} // numFields()
/**
* The type of field at the given offset
*
* Numeric constants can be found here: http://php.net/manual/en/mysqli.constants.php
*
* @return int
* @since Version 20120328
* @author Version 20120328, costmo
* @param resource $result The conenction resource returned by a query
* @param int $columnOffset The offset of the requested column (0 is the first column)
*/
public function fieldType( $result, $columnOffset )
{
$field = $this->fetchFieldObject( $result, $columnOffset );
return $field->type;
} // fieldType()
/**
* A uniform type of field at the given offset.
*
* Returns a string that represents a simplified field type for consistency across DBMS'
*
* We don't check every field type. This has been written for the field types that APP(ideas) uses commonly. It will probably need modification to suit the needs of others.
*
* This globs a lot of different types together. You may need to make them more fine-grained.
* We've also not tested that each of the supplied types return as expected. Especially the mysqli types often times return unexpected results,
* so be sure to test for your specific use before relying on this.
*
* For MySQL, if you choose to use an enum type for boolean, you could conceivably write a test case specific to your implementation for this to return "bool"
* but you would need to grab the enumerated values from the field and test them for known values to keep from confusing them from non-boolean ENUMs
*
* @return string
* @since Version 20120328
* @author Version 20120328, costmo
* @param resource $result The conenction resource returned by a query
* @param int $columnOffset The offset of the requested column (0 is the first column)
*/
public function simpleFieldType( $result, $columnOffset )
{
$field = $this->fetchFieldObject( $result, $columnOffset );
switch( $field->type )
{
case MYSQLI_TYPE_BIT:
case MYSQLI_TYPE_TINY:
case MYSQLI_TYPE_DECIMAL:
case MYSQLI_TYPE_NEWDECIMAL:
case MYSQLI_TYPE_SHORT:
case MYSQLI_TYPE_LONG:
case MYSQLI_TYPE_LONG:
case MYSQLI_TYPE_FLOAT:
case MYSQLI_TYPE_DOUBLE:
case MYSQLI_TYPE_LONGLONG:
case MYSQLI_TYPE_INT24:
case MYSQLI_TYPE_INTERVAL:
return "numeric";
break;
case MYSQLI_TYPE_TIMESTAMP:
case MYSQLI_TYPE_DATE:
case MYSQLI_TYPE_TIME:
case MYSQLI_TYPE_DATETIME:
case MYSQLI_TYPE_YEAR:
case MYSQLI_TYPE_NEWDATE:
return "date";
break;
case MYSQLI_TYPE_VAR_STRING:
case MYSQLI_TYPE_STRING:
case MYSQLI_TYPE_CHAR:
case MYSQLI_TYPE_TINY_BLOB: // for some reason, many strings (particularly 'text' fields) return that they are blobs
case MYSQLI_TYPE_MEDIUM_BLOB:
case MYSQLI_TYPE_LONG_BLOB:
case MYSQLI_TYPE_BLOB:
return "string";
break;
default:
return "unknown";
break;
// No native boolean or detectable BLOB type for MySQL/mysqli
}
} // simpleFieldType
/**
* Convert a database-formatted boolean value into something consistent with PHP.
*
* For MySQL, this assumes that the boolean field is an int that will contain either 1 or 0.
* You may want to use an ENUM type instead, in which case, the pgsql code can be used here
*
* @return bool
* @since Version 20120328
* @author Version 20120328, costmo
* @param string $booleanValue A boolean value retrieved from a database query
*/
public function fixBoolean( $booleanValue )
{
if( 1 === (int) strtolower( $booleanValue ) )
{
return TRUE;
}
else
{
return FALSE;
}
} // fixBoolean()
/**
* Fixes a boolean input value of any kind to one understood by the DBMS.
*
* For MySQL, this returns a 1 or 0
*
* @return int
* @since Version 20120328
* @author Version 20120328, costmo
* @param string $booleanValue A boolean value retrieved from a database query
*/
public function fixDbBoolean( $booleanValue )
{
$returnValue = 1;
if( (strlen( $booleanValue ) < 1) ||
(0 === $booleanValue) ||
('0' === $booleanValue) ||
(false === $booleanValue) ||
("false" === strtolower( $booleanValue )) ||
("f" === strtolower( $booleanValue )) ||
(NULL === $booleanValue) )
{
$returnValue = 0;
}
return $returnValue;
} // end fixDbBoolean
/**
* Inserts a blank record into the requested table and returns the value of the surrogate key of the new record.
*
* This allows us to use a "modify" method for saving new data rather than separate "add" and "modify"
*
* If one of your "requiredFields" is a string, you must enclose it in apostrophes on input
*
* @return int
* @since Version 20120328
* @author Version 20120328, costmo
* @param string $tableName The name of the table into which we are inserting
* @param string $fieldName The name of the field into which we are inserting
* @param mixed $requiredFields An array of other fields and values that must not be null on a new record insert
*/
public function insertBlank( $tableName, $fieldName, $requiredFields = FALSE )
{
if( !$requiredFields || !is_array( $requiredFields ) )
{
$insertQuery = "
INSERT
INTO " . $tableName . " ( " . $fieldName . " )
VALUES ( DEFAULT )
";
}
else
{
$fields = "";
$values = "";
foreach( $requiredFields as $field => $value )
{
$fields .= ", " . $field;
$values .= ", " . $value;
}
$insertQuery = "
INSERT
INTO " . $tableName . " ( " . $fieldName . $fields . " )
VALUES ( DEFAULT" . $values . " )
";
}
$mungedTableName = preg_replace( '/\"$/', '', $tableName );
$mungedTableName = $mungedTableName . "_" . $fieldName . "_seq";
$returnQuery = "SELECT LAST_INSERT_ID() FROM " . $tableName;
$this->beginTransaction( __FILE__, __LINE__ );
$this->query( $insertQuery, __FILE__, __LINE__ );
$this->endTransaction( __FILE__, __LINE__ );
$result = $this->query( $returnQuery, __FILE__, __LINE__ );
$row = $this->fetchRow( $result );
$returnValue = $row[0];
return $returnValue;
} // insertBlank()
/**
* Begins a database transaction if supported by the DBMS.
*
* Not supported in MySQL by default. YOu can alter this if you use InnoDB tables.
*
* @return void
* @since Version 20120328
* @author Version 20120328, costmo
* @param string $file The file making the query. Leave empty to report the DB connector file name.
* @param int $line The line number in the querying file. Leave empty to report the DB connector line number.
*/
public function beginTransaction( $file = false, $line = false )
{
return;
} // beginTransaction()
/**
* Ends (commits) a database transaction if supported by the DBMS.
*
* Not supported in MySQL by default. YOu can alter this if you use InnoDB tables.
*
* @return void
* @since Version 20120328
* @author Version 20120328, costmo
* @param string $file The file making the query. Leave empty to report the DB connector file name.
* @param int $line The line number in the querying file. Leave empty to report the DB connector line number.
*/
public function endTransaction( $file = false, $line = false )
{
return;
} // endTransaction()
/**
* Retrieves a value that can be inserted into the database as a date or timestamp indicating the current date and/or time
*
* @return string
* @since Version 20120328
* @author Version 20120328, costmo
* @param string $format The format of the string to return. One of 'database' 'epoch' or a PHP date() format. The default is 'database'
*/
public function getCurrentTimestamp( $format = 'database' )
{
$returnString = date( "Y-m-d H:i:s" );
if( "database" === $format )
{
// Do nothing. This is the default
}
else if( "epoch" === $format )
{
$returnString = time();
}
else
{
$returnString = date( $format );
}
return $returnString;
} // end getCurrentTimestamp
/**
* Returns -1 if the versions table cannot be found, the current version number otherwise
*
* @return int
* @since Version 20120328
* @author Version 20120328, costmo
*/
public function getCurrentVersion()
{
$returnValue = 0;
$sql = "SHOW TABLES LIKE '" . $this->mVersionsTable . "'";
$result = $this->query( $sql, __FILE__, __LINE__ );
if( $this->numRows( $result ) < 1 )
{
$returnValue = -1;
}
else
{
$sql = "SELECT MAX( version_number ) FROM " . $this->mVersionsTable;
$result = $this->query( $sql, __FILE__, __LINE__ );
$row = $this->fetchRow( $result );
$returnValue = (is_numeric( $row[0] )) ? $row[0] : 0;
}
return $returnValue;
}
/**
* Retrieves a list of all available files for possible upgrade/downgrade of the database schema.
*
* Will echo an error and exit if file validation does not pass
*
* @return array
* @since Version 20120328
* @author Version 20120328, costmo
* @param string $fromDir The input directory
*/
public function getUpgradeFiles( $fromDir )
{
$returnValue = array();
if( !is_dir( $fromDir ) )
{
return $returnValue;
}
$dh = opendir( $fromDir );
while( false !== ($file = readdir( $dh )) )
{
if( 1 === preg_match( "/\.php$/", $file ) )
{
$returnValue[] = $file;
}
}
sort( $returnValue, SORT_NUMERIC );
// get out of here if the upgrade files are not in a valid sequence
if( !$this->validateUpgradeFiles( $returnValue ) )
{
exit();
}
return $returnValue;
}
/**
* Makes sure there are no skips in the version numbers
*
* @return bool
* @since Version 20120328
* @author Version 20120328, costmo
* @param array $files The retrieved files
*/
public function validateUpgradeFiles( $files )
{
$returnValue = true;
$files = AiUtil::mustBeArray( $files );
$lastFileName = 0;
foreach( $files as $index => $fileName )
{
$split = preg_split( "/\./", $fileName );
if( (int) $split[0] !== $lastFileName + 1 )
{
echo "File name: " . $fileName . " represents a skip from number " . $lastFileName . ". The upgrade cannot continue.\n";
$returnValue = false;
$lastFileName = $split[0];
}
else
{
$lastFileName++;
}
}
return $returnValue;
}
/**
* Performs an upgrade to the latest version or downgrade a single version
*
* @return string
* @since Version 20120328
* @author Version 20120328, costmo
* @param int $fromVersion The user's current schema version
* @param string $direction "up" for an upgrade or "down" for a downgrade
*/
public function doUpgrade( $fromVersion, $direction )
{
$files = $this->getUpgradeFiles( $_ENV["PWD"] . "/data/mysql" );
// make sure there are files and the input directory exists
if( count( $files ) < 1 )
{
return "Either there were no files in the updates/data/mysql directory or the directory was not found.";
}
$lastFile = end( $files );
$split = preg_split( "/\./", $lastFile );
$lastVersion = $split[0];
// exit if we can't or don't need to up/downgrade
if( "up" === $direction && $fromVersion >= $lastVersion )
{
return "Already up to date.";
}
else if( "down" === $direction && $fromVersion > $lastVersion )
{
return "I do not have a file that is late enought to perform this downgrade. Your version is " . $fromVersion . " and the latest downgrade file is " . $lastFile;
}
if( "up" === $direction )
{
echo "Upgrading from version " . $fromVersion . " to " . $lastVersion . "...\n";
// Run the updates
foreach( $files as $index => $fileName )
{
if( ($index + 1) > $fromVersion )
{
$split = preg_split( "/\./", $fileName );
echo "Upgrading to version " . $split[0] . "...\n";
require_once( $_ENV["PWD"] . "/data/mysql/" . $fileName );
$className = DbUpdater . $split[0];
$update = new $className();
$sqlArray = $update->upgrade();
$this->beginTransaction( __FILE__, __LINE__ ); // cause an explicit rollback if any one of the intermediate queries fails
foreach( $sqlArray as $index => $sql )
{
echo $sql . "\n";
$this->query( $sql, __FILE__, __LINE__ );
}
$this->recordSchemaChange( "add", $split[0] );
$this->endTransaction( __FILE__, __LINE__ );
} // if( ($index + 1) > $fromVersion ) )
} // foreach( $files as $index => $fileName )
}
else if( "down" === $direction ) // run a downgrade
{
echo "Downgrading version " . $fromVersion . "...\n";
if( $fromVersion > 0 )
{
require_once( $_ENV["PWD"] . "/data/mysql/" . $fromVersion . ".php" );
$className = DbUpdater . $fromVersion;
$update = new $className();
$sqlArray = $update->downgrade();
$this->beginTransaction( __FILE__, __LINE__ ); // cause an explicit rollback if any one of the intermediate queries fails
foreach( $sqlArray as $index => $sql )
{
echo $sql . "\n";
$this->query( $sql, __FILE__, __LINE__ );
}
$this->recordSchemaChange( "delete", $fromVersion );
$this->endTransaction( __FILE__, __LINE__ );
}
}
return "\n" . ucfirst( $direction ) . "grade complete.";
}
/**
* Records a schema change in the database
*
* @return void
* @since Version 20120328
* @author Version 20120328, costmo
* @param string $action The action to perform. Either "add" or "delete"
* @param int $versionNumber The version number to record
*/
public function recordSchemaChange( $action, $versionNumber )
{
// get out of here if the action isn't "add" or "delete"
if( !in_array( strtolower( $action ), array( "add", "delete" ) ) )
{
return;
}
// Make the query
$sql = "INSERT INTO " . $this->mVersionsTable . " ( version_number ) VALUES( " . AiUtil::MustBeNumber( $versionNumber ) . " )";
// overwrite the query if this is a downgrade
if( strtolower( "delete" ) === $action )
{
$sql = "DELETE FROM " . $this->mVersionsTable . " WHERE version_number = " . AiUtil::MustBeNumber( $versionNumber );
}
$this->query( $sql, __FILE__, __LINE__ );
}