<?php
require_once(dirname(__FILE__) . '/../base.inc.php');

/***********************************

 database class

 how to use:

1. basic, easy, make sure you do your own sql param escaping here if necessary
 $fusiondb = new db('fusion');
 $rows = $fusiondb->exec_query('SELECT tacos FROM fridays');
 foreach($rows as $row)
 	do_something();

2. still easy, but supply a param/val array to bind to prepared statement
 $fusiondb = new db('fusion');
 $query = 'SELECT tacos FROM fridays WHERE some_integer = :some_int AND some_string = :some_str';
 $bind_array = array(
 	':some_int' => 1,
 	':some_str' => 'moretaco');
 $rows = $fusiondb->exec_query($query, $bind_array);
 foreach($rows as $row)
 	do_something();

3. alternatively, you could do it all on your own, as well:
 $fusiondb = new db('fusion');
 $query = 'SELECT tacos FROM fridays WHERE some_integer = :some_int AND some_string = :some_str';
 $fusiondb->query($query);
 $fusiondb->bind(':some_int', 1);
 $fusiondb->bind(':some_str', 'THE STRING', PDO::PARAM_STR); // this time we overrode the checking for param type with specified
 $fusiondb->exec();
 $rows = $fusiondb->fetch_rows();



oh, you need some error info?

echo $fusiondb->get_last_error();
or

print_r($fusiondb->get_errors());


or some debug information from the params in the preped statement?

echo $fusion->get_debug_dump_params();

***********************************/

/**
 * db class
 */
class db {

	private $type;
	private $host;
	private $user;
	private $pass;
	private $port;
	private $dbname;
	private $persist;
	private $connected;

	private $pdo;
	private $statement;
	private $sql;
	private $bind_array;

	/*
	insert/replace/select/etc.
	*/
	private $sql_type;

	private $reconnect_count;
	private $errors;
	private $last_error;

	/** 
	 * constructor
	 * @param string $which_db - the string containing the name of the database to attempt to connect to, must be present in config.inc.php, default is fusion
	 */
	public function __construct($which_db = 'fusion') {

		// set all of our defaults before we attempt to load configuration
		$this->connected = false;
		$this->sql = '';
		$this->errors = array();
		$this->last_error = '';
		$this->reconnect_count = 5;

		if ($this->load_configuration($which_db)) {

			$this->connect();

		} else {

			$this->set_error("__construct({$which_db}): Unable to construct", DB_ERROR_CONSTRUCT);
		}
	}


	/** 
	 * $db->load_configuration() - grab settings from config.inc.php and populate db object
	 * @param string $which_db - the string containing the name of the database to attempt to connect to, must be present in config.inc.php, default is fusion
	 * @return bool - successful load of configuration
	 */
	private function load_configuration($which_db = 'fusion') {

		global $cfg;

		$cfg_db = '';
		if (isset($cfg['database'][$which_db]) && is_array($cfg['database'][$which_db])) {

			$cfg_db = $cfg['database'][$which_db];
		}

		if (empty($cfg_db)) {

			$this->set_error("load_configuration({$which_db}): No config found for {$which_db}", DB_ERROR_CONFIG_NONE);
			return false;
		}

		$this->type = empty($cfg_db['type']) ? 'mysql' : $cfg_db['type'];
		$this->host = empty($cfg_db['host']) ? 'localhost' : $cfg_db['host'];
		$this->user = $cfg_db['user'];
		$this->pass = $cfg_db['pass'];
		$this->port = $cfg_db['port'];
		$this->dbname = $which_db;
		$this->persist = !isset($cfg_db['persist']) ? true : (bool) $cfg_db['persist'];

		// check if we really have the data we need to continue
		if (empty($this->type) ||
			empty($this->host) ||
			empty($this->user) ||
			empty($this->pass) ||
			empty($this->dbname) ||
			!isset($this->persist)) {

			$this->set_error("load_configuration({$which_db}): Config variables incomplete for {$which_db}", DB_ERROR_CONFIG_INCOMPLETE);
			return false;
		}

		return true;
	}

	/** 
	 * $db->connect() - make connection and store in $this->pdo
	 * @return bool - successful connection status, if false errors are set and can be retrieved with
	 */
	private function connect($recursive = false) {

		$dsn = "{$this->type}:host={$this->host};dbname={$this->dbname};";

		if (!empty($this->port)) {

			$dsn .= "port={$this->port};";
		}

		$options = array(
			PDO::ATTR_PERSISTENT	=> (bool) $this->persist,
			PDO::ATTR_ERRMODE		=> PDO::ERRMODE_EXCEPTION,
			);

		try {

			$this->pdo = new PDO($dsn, $this->user, $this->pass, $options);
			$this->connected = true;

		} catch (PDOException $e) {

			// if this is the first time we've called connect
			// and it failed, then we try for a specified amount of time before we give up
			if (!$recursive) {

				global $cfg;

				$database_connection_timeout = 10;
				if (defined('DEFAULT_DATABASE_CONNECTION_TIMEOUT'))
					$database_connection_timeout = intval(DEFAULT_DATABASE_CONNECTION_TIMEOUT);

				if (!empty($cfg['database']['connection_timeout']))
					$database_connection_timeout = $cfg['database']['connection_timeout'];

				$start = time();
				while (time() - $start <= $database_connection_timeout) {

					if ($this->connect($recursive = true)) {

						$this->clear_errors();
						return true;
					}
					sleep(1);
				}

				$this->set_error("connect(\$dsn = $dsn;user={$this->user};pass={$this->pass}): " . $e->getMessage(), DB_ERROR_CONNECT);
				exit(_('Unable to connect to MySQL. Is the database running?'));
			}

			return false;
		}

		$this->clear_errors();
		return true;
	}

	/**
	 * $db->last_insert_id() - return the last insert id (from insert/replace)
	 * @return int - the last insert id
	 */
	public function last_insert_id() {

		try {

			return $this->pdo->lastInsertId();

		} catch (PDOException $e) {

			$this->set_error("last_insert_id(): " . $e->getMessage(), DB_ERROR_LAST_INSERT_ID);
			return false;
		}
	}

	/**
	 * $db->last_rows_affected - return last rows affected (from delete/update/insert/replace)
	 * @return int - the last count of affected rows
	 */
	public function last_row_count() {

		try {

			return $this->statement->rowCount();

		} catch (PDOException $e) {

			$this->set_error("last_row_count(): " . $e->getMessage(), DB_ERROR_LAST_ROW_COUNT);
			return false;
		}
	}

	/**
	 * $db->exec() - execute prepared $this->statement
	 * @return bool - successful execution of $db->statement
	 */
	public function exec() {

		try {

			return $this->statement->execute();

		} catch (PDOException $e) {

			// if the server was restarted, just try again
			if (strpos($e->getMessage(), 'server has gone away') !== false) {

				$this->statement->closeCursor();
				$this->statement = null;
				$this->pdo = null;
				$this->connected = false;

				do {

					sleep(2);
					if ($this->connect())
						break;

				} while (!$this->connected && $this->reconnect_count-- >= 0);

				// now see if the fruits of our labor have paid off!
				if ($this->connected) {

					// Mention in the log that we are re-running the command (due to the
					// potential that this looping never ends)
					$this->set_error("Reconnected MySQL with error: " . $e->getMessage(), DB_ERROR_EXEC);

					// we have to set everything up again and re-exec()
					return $this->exec_query($this->sql, $this->bind_array);
				}
			}

			$this->set_error("exec(): " . $e->getMessage(), DB_ERROR_EXEC);
			return false;
		}
	}

	/** 
	 * $db->exec_query() - execute a sql query (and also optionally set up statement and bind params)
	 * - this function allows you to prepare a statement, bind params/placeholders to values, execute and retrieve rows all in one function
	 * - alternatively, you could call the other public methods to set up your query and bind on your own, call this and retrieve the rows as well
	 * @param string $sql - the sql statement itself
	 * @param mixed $bind_array - either an empty string or an array in the format of array(':param' => 'value');
	 * @return mixed - 
	 *				bool  - false - if something goes wrong with binding $bind_array to params, errors are set
	 *				bool  - false - if the query succeeded and something went wrong fetching rows, errors are set
	 *				int   - last_row_count() - if the query succeeded and there is no data[rows] to return...
	 *				int   - last_insert_id() - if last_row_count() returned 1, then we check last_insert_id() to get the last insert id
	 *				array - fetch_rows() - if the query succeeded and there is data[rows] to return, it returns that data as an array
	 *				bool  - false - if the prepared statement was unable to be executed, errors are set
	 */
	public function exec_query($sql = '', $bind_array = '') {

		// get a bind array string for error msg if we need
		$bind_array_str = !empty($bind_array) && is_array($bind_array) && count($bind_array) ? print_r($bind_array, true) : "''";

		// prepare query if supplied
		if (!empty($sql)) {

			$this->query($sql);
		}

		// bind params if supplied
		if (!empty($bind_array)) {
			if (is_array($bind_array)) {
				foreach ($bind_array as $param => $value) {

					if (strpos($sql, $param) !== false) {
						if (!$this->bind($param, $value)) {

							$this->set_error("exec_query({$sql}, {$bind_array_str}): Error binding $param to $value", DB_ERROR_BIND);
							return false;
						}
					}
				}
			}
		}

		// execute and attempt to return an array of row data or the amount of rows affected
		$exec = false;
		try {

			$exec = $this->exec();

		} catch (PDOException $e) {

			$this->set_error("exec_query({$sql}, {$bind_array_str}): " . $e->getMessage(), DB_ERROR_EXEC_QUERY);
			return false;
		}

		if ($exec) {

			if (in_array($this->sql_type, array('insert', 'replace')))
				return $this->last_insert_id();

			if (in_array($this->sql_type, array('truncate')))
				return true;

			if (in_array($this->sql_type, array('select', 'describe', 'show'))) {

				$rows = $this->fetch_rows();
				if (is_array($rows))
					return $rows;
			}

			return $this->last_row_count();
		}

		// failsafe lets just assume we failed
		$this->set_error("exec_query({$sql}, {$bind_array_str}): Failed to execute '{$this->sql}'", DB_ERROR_EXEC_QUERY);
		return false;
	}

	/**
	 * $db->fetch_rows() - get rows from exec'd statement
	 * @return mixed - array containing data if fetchAll succeeded, false otherwise
	 */
	public function fetch_rows() {

		$rows = array();
		try {

			$rows = $this->statement->fetchAll(PDO::FETCH_ASSOC);

		} catch (PDOException $e) {

			$this->set_error("fetch_rows(): " . $e->getMessage(), DB_ERROR_FETCH_ROWS);
			return false;
		}
		return $rows;
	}

	/**
	 * $db->bind() - optionally attempt to determine param_type, and bind parameters to prepared query ($statement)
	 * @param string $param - the param [placeholder] to bind $value to
	 * @param mixed $value - the value to bind to specified $param placeholder, can be string null bool or int
	 * @param mixed $type - empty string will determine whether bind() attempts to determine the param type, or specify the PDO::PARAM_* constant here
	 * @return bool - true if successful bind, false otherwise
	 */
	public function bind($param, $value, $type = '') {

		if (empty($type)) {

			// try and automagically determine the pdo:: type to bind to
			switch (true) {
				
				case is_int($value):
					$type = PDO::PARAM_INT;
					break;

				case is_bool($value):
					$type = PDO::PARAM_BOOL;
					break;

				case is_null($value):
					$type = PDO::PARAM_NULL;
					break;

				default:
					$type = PDO::PARAM_STR;
					break;
			}
		}

		// save the param in case of some kind of db catastrophy (reconnection)
		$this->bind_array[$param] = $value;

		try {

			return $this->statement->bindValue($param, $value, $type);

		} catch (PDOException $e) {

			$this->set_error("bind(): " . $e->getMessage(), DB_ERROR_BIND);
			return false;
		}
	}

	/**
	 * $db->query() - set query var, prepare query and store in $statement
	 * @param string $sql - the sql statement itself
	 * @return bool - true if successful preparation, false if otherwise and errors are set
	 */
	public function query($sql) {
		
		$this->sql = $sql;

		// determine sql type (select/insert/replace/delete/update/etc.)
		$sql_type = '';
		if (preg_match('/\S+/', ltrim($sql), $matches) == 1)
			$sql_type = strtolower($matches[0]);

		$this->sql_type = $sql_type;

		try {

			$this->statement = $this->pdo->prepare($sql);

		} catch (PDOException $e) {

			$this->set_error("query({$sql}): " . $e->getMessage(), DB_ERROR_PREPARE);
			return false;
		}

		// reset the bind array
		$this->bind_array = array();

		return true;
	}


	/**
	 * $db->begin_transaction() - start a transaction
	 * @return bool success/failure
	 */
	public function begin_transaction() {

		try {

			return $this->pdo->beginTransaction();

		} catch (PDOException $e) {

			$this->set_error("begin_transaction(): " . $e->getMessage(), DB_ERROR_TRANS_BEGIN);
			return false;
		}
	}


	/**
	 * $db->commit() - commit a transaction
	 * @return bool success/failure
	 */
	public function commit() {

		try {

			return $this->pdo->commit();

		} catch (PDOException $e) {

			$this->set_error("commit(): " . $e->getMessage(), DB_ERROR_TRANS_COMMIT);
			return false;
		}
	}


	/**
	 * $db->commit() - commit a transaction
	 * @return bool success/failure
	 */
	public function roll_back() {

		try {

			return $this->pdo->rollBack();

		} catch (PDOException $e) {

			$this->set_error("roll_back(): " . $e->getMessage(), DB_ERROR_TRANS_ROLLBACK);
			return false;
		}
	}


	/**
	 * $db->get_last_error() - return any error information
	 * @return string - contains the last set error message
	 */ 
	public function get_last_error() {

		if (empty($this->last_error)) {

			return false;

		} else {

			return $this->last_error;
		}
	}

	/**
	 * $db->get_errors() - return an array of all of the errors that have occured since instantiation
	 * @return array - containing all errors present, empty array if none
	 */
	public function get_errors() {

		$this->initialize_errors();

		return $this->errors;
	}


	/**
	 * $db->clear_errors() - resets the errors array
	 */	
	public function clear_errors() {

		$this->errors = null;
	}

	
	/**
	 * $db->initialize_errors() - sets the errors to be an array if nan
	 */	
	public function initialize_errors() {

		if (empty($this->errors) || !is_array($this->errors)) {

			$this->errors = array();
		}
	}


	/**
	 * $db->set_error() - sets the last_error and also appends to the errors array
	 * @param string $error_msg - the message to push to the error array
	 * @param int $error_code - the defined constant associated with this error
	 */
	public function set_error($error_msg, $error_code = DB_ERROR_NONE) {
		
		$this->initialize_errors();

		$this->last_error = $error_msg;
		$this->last_error_code = $error_code;

		// Write error to a db error log and get the params
		$debug_params = $this->get_debug_dump_params();
		file_put_contents(DEFAULT_DB_ERROR_FILE, $error_code . ': ' . $error_msg, FILE_APPEND);
		file_put_contents(DEFAULT_DB_ERROR_FILE, $debug_params, FILE_APPEND);

		array_push($this->errors, array($error_code => $error_msg));
	}

	/**
	 * $db->get_debug_dump_params() - return debug information regarding prepared statement
	 * @return string - runs PDO::debugDumpParams and dumps to string which is returned
	 */
	public function get_debug_dump_params() {

		$output = '';
		try {

			ob_start();
			$this->statement->debugDumpParams();
			$output = ob_get_clean();

		} catch (PDOException $e) {
			$output = $e->getMessage();
		}

		return $output;
	}

	/**
	 * db::errno() - return a readable human string from supplied value
	 * @param mixed $constant - the value to check against defined constants
	 * @return string - human readable form of $constant
	 */
	public static function errno($constant = '') {

		switch ($constant) {
			case DB_ERROR_NONE:
				return 'DB_ERROR_NONE';
				break;
			case DB_ERROR_CONSTRUCT:
				return 'DB_ERROR_CONSTRUCT';
				break;
			case DB_ERROR_CONFIG_NONE:
				return 'DB_ERROR_CONFIG_NONE';
				break;
			case DB_ERROR_CONFIG_INCOMPLETE:
				return 'DB_ERROR_CONFIG_INCOMPLETE';
				break;
			case DB_ERROR_CONNECT:
				return 'DB_ERROR_CONNECT';
				break;
			case DB_ERROR_LAST_ROW_COUNT:
				return 'DB_ERROR_LAST_ROW_COUNT';
				break;
			case DB_ERROR_LAST_INSERT_ID:
				return 'DB_ERROR_LAST_INSERT_ID';
				break;
			case DB_ERROR_EXEC:
				return 'DB_ERROR_EXEC';
				break;
			case DB_ERROR_EXEC_QUERY:
				return 'DB_ERROR_EXEC_QUERY';
				break;
			case DB_ERROR_FETCH_ROWS:
				return 'DB_ERROR_FETCH_ROWS';
				break;
			case DB_ERROR_BIND:
				return 'DB_ERROR_BIND';
				break;
			case DB_ERROR_PREPARE:
				return 'DB_ERROR_PREPARE';
				break;
			case DB_ERROR_TRANS_BEGIN:
				return 'DB_ERROR_TRANS_BEGIN';
				break;
			case DB_ERROR_TRANS_COMMIT:
				return 'DB_ERROR_TRANS_COMMIT';
				break;
			case DB_ERROR_TRANS_ROLLBACK:
				return 'DB_ERROR_TRANS_ROLLBACK';
				break;
			default:
				return '';
				break;
		}
	}
}
