What do you think to my Database class?

It is designed so you can dump it in a PHP file and open in your browser with no need for actual db access.

Some people say to only use 2 classes, not 3 deep. What do you think?


<?php

//simulate $_POST
$_POST['name'] = "Dave Smith extra long name here";
$_POST['username'] = "smith123";

$_POST['searchname'] = "Dave";
$_POST['searchUserID'] = "555";


class Database {

	//plus lots of other stuff (basically everything else not already shown below)

	protected function escape($val) {
	
		//mysql_real_escape_string
		return $val;
		
	}
	
	protected function query($sql) {
	
		//mysql_query
		return $sql;
	
	}

}

class DatabaseControl extends Database {

	public $connection;
	public $errors = array();

	public function __construct($db) {
		// grab db connection variable
		$this->connection = $db;
	}

	//logs errors when preparing to add/update record
	public function addError($num, $text) {
	
		$this->errors[$num] = $text;
	
	}
	
	public function go($class, $method) {
	
		call_user_func(array($class, $method), $this);
	
	}
	
	public function checkMaxCharacters($num, $text, $val) {
	
		if(strlen($val)>20) {
			
			$this->addError($num, $text);
				
		} else {
		
			return $val;
		
		}
	
	}

}

class User extends DatabaseControl {

	public static function findUser() {
		
		$name = parent::escape($_POST['searchname']);
		$userID = parent::escape($_POST['searchUserID']);
		$sql = "SELECT Name FROM users WHERE `Name` = '{$name}' && UserID = '{$userID}'";
		echo "THIS WAS COMPLETED: " . parent::query($sql);
	
	}
	
	public static function addUser($object) {
		
		//check input
		$nameEntered = $object->checkMaxCharacters(1, "Name has too many characters", $_POST['name']);
		$usernameRequested = $object->checkMaxCharacters(2, "Username has too many characters", $_POST['username']);
		
		$errors = $object->errors;
		
		if(count($errors)>0) {
		
			//errors so don't add
			foreach($errors as $v) {
			
				echo $v . "<br />";
			
			}
		
		} else {
		
			//good to go
			$nameEntered = parent::escape($nameEntered);
			$usernameRequested = parent::escape($usernameRequested);
			$sql = "INSERT INTO users (`name`, `username`) VALUES ('{$nameEntered}', '{$usernameRequested}')";
			echo "THIS WAS COMPLETED: " . parent::query($sql);
	
		}
	
	}
	
}

$db = new Database();

$getUser = new DatabaseControl($db);
$getUser -> go("User", "findUser");

echo "<br /><br />";

$addUser = new DatabaseControl($db);
$addUser -> go("User", "addUser");

?>

I like my database class better.


$db = new PDO($connectionString);

:wink:

Ok, seriously, my db is pretty much purely PDO, though I use this wrapper.


namespace Gazelle;

/**
 * Gazelle core database extends the PDO library.
 * @author Michael
 *
 */
class Database extends \\PDO {
	
	/**
	 * CONSTRUCT. Convert the configuration array into a DSN and pass that down to
	 * PDO.
	 * @param array $config
	 */
	public function __construct( $config ) {
		assert ($config['database'] && $config['user'] && $config['password']);
	
		if (isset($config['dsn'])) {
			// If a DSN is set use it without question.
			parent::__construct($config['dsn'], $config['user'], $config['password']);
		} else {
			// Otherwise hash the vars we were given into a DSN and pass that.
			$params = array(
				'driver' => isset($config['driver']) ? $config['driver'] : 'mysql',
				'database' => $config['database'],
				'user' => $config['user'],
				'password' => $config['password'],
				'server' => isset($config['server']) ? $config['server'] : 'localhost',
				'port' => isset($config['port']) ? $config['port'] : '3306'
			);
		
			// Start underlying PDO library.
			parent::__construct("{$params['driver']}:dbname={$params['database']};host={$params['server']};port={$params['port']}", 
				$params['user'],
				$params['password']
			);
		}
	}
	
	/**
	 * Prepare and execute sql with it's parameters.
	 * @param string $sql
	 * @param array|null $params
	 */
	protected function prepareAndExecute( $sql, $params = null ) {
		
		$s = $this->prepare( $sql );

		if ( is_array($params)) {
			$s->execute( $params );
		} else {
			$s->execute();			
		}
		
		if ($s->errorCode() != '00000') {
			throw new DatabaseException ( print_r($s->errorInfo(), true) );
		}
		
		return $s;
	}
	
	/**
	 * Run a query and return the result in a form suited to the result.
	 * You should be able to predict the return based on the text of
	 * your query, but if unsure use query all.
	 * 
	 * @param string sql statement - required
	 * @param array sql parameters - optional
	 * 
	 * @return mixed
	 */
	public function quickQuery($sql, $params ) {		
		$s = $this->prepareAndExecute($sql, $params);
				
		$columns = $s->columnCount();
		$rows = $s->rowCount();
	
		// A single column and row - return the value found.
		if ( $rows == 1 && $columns == 1 ) {
			return $s->fetchColumn();
		} 
		// A pair of columns we presume column 1 is the index and column 2 the value.
		// We maintain this format even if there was only a single row.
		else if ( $columns == 2 ) {
			while ($row = $s->fetch( parent::FETCH_NUM )) {
				$r[$row[0]] = $row[1];
			}
			return $r;
		} 
		// Single row - return it.
		else if ( $rows == 1 ) {
			return $s->fetch( parent::FETCH_ASSOC );
		} 
		// Single column - return it
		else if ( $columns == 1 ) {
			return $s->fetchAll( parent::FETCH_COLUMN );
		}
		// Just shoot back everything.
		else {
			return $s->fetchAll( parent::FETCH_ASSOC );
		}
	}
	
	/**
	 * Get all results of a query. Unlike quickQuery, queryAll
	 * doesn't try to figure out your intent - it returns
	 * a full associative array.
	 * 
	 * @param string $sql
	 * @param array $params
	 */
	public function queryAll( $sql, array $params = array() ) {
		$s = $this->prepareAndExecute($sql, $params);
		return $s->fetchAll( parent::FETCH_ASSOC );	
	}
}

If anyone can come up with a better name for #quickQuery I’d appreciate it. No, I don’t want to override the #query method of PDO.

Anyway, the PDO library is a great DB class. I moved to it abandoning a db class I’d made myself and used successfully for 5 years.

Also, for those saying quickQuery and it’s buddy prepareAndExecute are wasteful note that the PDO methods for handling statements remain exposed and I do make use of them. The two new methods here are for quick checks.

Binding input arguments to post data doesn’t result in a very reusable interface. I would recommend passing the data in arguments, rather than relying on its existence in the post array.

This, just screams non-reuse.


	public static function findUser() {
		
		$name = parent::escape($_POST['searchname']);
		$userID = parent::escape($_POST['searchUserID']);
		$sql = "SELECT Name FROM users WHERE `Name` = '{$name}' && UserID = '{$userID}'";
		echo "THIS WAS COMPLETED: " . parent::query($sql);
	
	}

particularly:


		$name = parent::escape($_POST['searchname']);
		$userID = parent::escape($_POST['searchUserID']);

That decision makes the entire thing dependent on the post, very bad idea in terms or reuse and maintainability. Change a post field name and you need to update not only the application layer but the model layer also.


echo "THIS WAS COMPLETED: " . parent::query($sql);


The other thing is NEVER, print output in the the model, unless merely debugging. Return true, false, throw an exception to allow the outer layer to handle the situation in terms of display. Just never print anything in the model.

In terms of adapter this is the standard interface I use. This makes it possible to swap out PDO, MySQL, and MySQLi, since they all cohere to the same interface, including bound arguments.


interface MCPDB {
	public function connect($strHost,$strUser,$strPwd,$strDb);
	public function disconnect();
	public function query($strSQL,$arrBind=array());
	public function affectedRows();
	public function lastInsertId();
	public function escapeString($strValue);
	public function isConnected();
}

Example of MySQL adpater:


<?php
$this->import('App.Core.Resource');
$this->import('App.Core.DB');
class MCPMySQL extends MCPResource implements MCPDB {

	private 
	
	/*
	* Connection resource identifier
	*/
	$_objLink;
	
	/*
	* Connects to database
	*
	* @param str host name
	* @param str user name
	* @param str user password
	* @param str database name
	*/
	public function connect($strHost,$strUser,$strPwd,$strDb) {
		$this->_objLink = mysql_connect($strHost,$strUser,$strPwd);
		
		if(!$this->_objLink) {
			$this->_objMCP->triggerError('Unable to establish database connection');
		}
			
		if(!mysql_select_db($strDb,$this->_objLink)) {
			$this->_objMCP->triggerError('Unable to select database');
		}
	}

	/*
	* Disconnects from database
	*/
	public function disconnect() {
		if(!mysql_close($this->_objLink)) {
			$this->_objMCP->triggerError('Unable to close database connection');
		}
	}
	
	/*
	* Queries database returning result set as associative array
	*
	* @param str query
	* @param array bound arguments (necessary for consistent interface for PDO and MySQLi adapters)
	* @return array result set associative array
	*/
	public function query($strSQL,$arrBind=array()) {

		/*
		* Fake/replicate binding 
		*/
		if(!empty($arrBind)) {
			$strSQL = $this->_rewriteQuery($strSQL,$arrBind);
		}
		
		$objResult = mysql_query($strSQL,$this->_objLink);
		
		if(!$objResult) {
			$this->_objMCP->triggerError('SQL Query invalid');
		}
		
		$arrRows = array();
		if(strpos($strSQL,'SELECT') === 0 || strpos($strSQL,'DESCRIBE') === 0 || strpos($strSQL,'SHOW') === 0) {
			while($arrRow = mysql_fetch_assoc($objResult)) {
				$arrRows[] = $arrRow;
			}
		} else if(strpos($strSQL,'INSERT') === 0) {
			return $this->lastInsertId();
		} else if(strpos($strSQL,'UPDATE') === 0) {
			return $this->affectedRows();
		}
		
		return $arrRows;		
	}
	
	/*
	* Number of rows affected by the last query
	*
	* @return int number of affected rows
	*/
	public function affectedRows() {
	
		$affectedRows = mysql_affected_rows($this->_objLink);
		
		if($affectedRows < 0) {
			$this->_objMCP->triggerError('Last query failed so number of affected rows could not be determined');
		}
		
		return $affectedRows;
	
	}
	
	/*
	* Insert id of last query
	*
	* @return mix last insert id
	*/
	public function lastInsertId() {
		return mysql_insert_id($this->_objLink);	
	}
	
	/*
	* Escapes string for proper input into query statement
	*
	* @param str string to escape
	* @return escaped string value
	*/
	public function escapeString($strValue) {
		$strEscapedValue = mysql_real_escape_string($strValue,$this->_objLink);
		
		if($strEscapedValue === false) {
			$this->_objMCP->triggerError('String escape failed');
		}
		
		return $strEscapedValue;
		
	}
	
	/*
	* Has database connection been established?
	* @return bool
	*/
	public function isConnected() {
		return $this->_objLink === null?false:true;
	}
	
	/*
	* Internal method used to rewrite supplied queries with
	* question mark or named placeholders. Since, default MySQL
	* adapter doesn't support bound arguments the the behvior
	* must be "faked" to keep a consistent interface across
	* adapters that do actually supply this capability such as; PDO
	* and MySQLi.
	* 
	* @param str SQL w/ placeholders 
	* @param array bindings
	* @return str rewritten SQL
	*/
	protected function _rewriteQuery($strSQL,$arrBind) {
		
		/*
		* Determines whether matching will be name or ? placeholder based 
		*/
		$named = 'false';
		
		/*
		* Pttern to match against; defaults to ? placeholder 
		*/
		$pattern = '\\?';
		
		/*
		* Name based placeholder named and pattern change
		*/
		if(strpos($strSQL,'?') === false) {
			$pattern = '('.implode('|',array_keys($arrBind)).')';
			$named = 'true';
		}
		
		/*
		* begin building replacement function 
		*/
		$func = 'static $i=0;$values = array();$named = '.$named.';';

		foreach($arrBind as $mixIndex=>$mixValue) {

			/*
			* NULL, integer and string mutations 
			*/
			if($mixValue === null) {
				$mixValue = '\\'NULL\\'';
			} else if(is_int($mixValue)) {
				$mixValue = $this->escapeString($mixValue);
			} else {
				$mixValue = "\\"'".$this->escapeString($mixValue)."'\\"";
			}
	
			/*
			* Push onto replacement function values array 
			*/
			$func.= '$values[\\''.$mixIndex.'\\'] = '.$mixValue.';';

		};

		$func.= 'return $named?$values[$matches[0]]:$values[$i++];';

		/*
		* End building replacement function; rebuild SQL. 
		*/
		return preg_replace_callback("/$pattern/",create_function('$matches',$func),$strSQL);
		
	}

}
?>

The private rewrite function allows the standard adapter to “support” a bound argument interface.

Though my goal has been to support interchangable adapters, perhaps different from your own.

Here is a simple example of one of my models. No magic, or anything just straight forward, reusable, isolated business logic methods to accomplish common tasks.


/*
* Site data access layer 
*/
class MCPDAOSite extends MCPDAO {
	
	/*
	* List all sites
	* 
	* @param str select fields
	* @param str where clause
	* @param order by clause
	* @param limit clause
	* @return array users
	*/
	public function listAll($strSelect='s.*',$strFilter=null,$strSort=null,$strLimit=null) {
		
		/*
		* Build SQL 
		*/
		$strSQL = sprintf(
			'SELECT
			      %s %s
			   FROM
			      MCP_SITES s
			      %s
			      %s
			      %s'
			,$strLimit === null?'':'SQL_CALC_FOUND_ROWS'
			,$strSelect
			,$strFilter === null?'':"WHERE $strFilter"
			,$strSort === null?'':"ORDER BY $strSort"
			,$strLimit === null?'':"LIMIT $strLimit"
		);
		
		$arrSites = $this->_objMCP->query($strSQL);
		
		if($strLimit === null) {
			return $arrSites;
		}
		
		return array(
			$arrSites
			,array_pop(array_pop($this->_objMCP->query('SELECT FOUND_ROWS()')))
		);
		
	}
	
	/*
	* Fetch site data by sites id 
	* 
	* @param int site id
	* @param str select fields
	* @return array site data
	*/
	public function fetchById($intId,$strSelect='*') {
		$strSQL = sprintf(
			'SELECT %s FROM MCP_SITES WHERE sites_id = %s'
			,$strSelect
			,$this->_objMCP->escapeString($intId)
		);		
		return array_pop($this->_objMCP->query($strSQL));
	}
	
}
?>

You realise that you can have PDO throw exceptions on error, right?

And since PdoStatement implements an iterator interface, this isn’t really needed. You can simply iterate over the statement. If you want, you can call setFetchMode on the statement.

One thing I don’t like about the iterator interface is rewind() requirement. If the result set is forward only, rewind() makes no sense.

With closures, currently favouring this style…


$f = $db->prepare('SELECT * FROM Table t WHERE id = ?');
$i = $f(1);
while ($r = $i())
{
...
}

I’ll definitely keep this in mind now that I’m starting work with this in earnest. To date I’ve just been dabbling with PDO.