From 9a2d593eff9d34736f559913b017e6c5e7967848 Mon Sep 17 00:00:00 2001 From: Joshua Sherman Date: Sun, 12 Jan 2014 23:24:41 -0500 Subject: [PATCH] Reworked the database class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Got rid of all of that object bloatin’ nonsense. --- classes/Database.php | 427 ++++++++++++++++++++++++---- classes/Database/Common.php | 139 --------- classes/Database/PDO/Common.php | 278 ------------------ classes/Database/PDO/MySQL.php | 45 --- classes/Database/PDO/PostgreSQL.php | 45 --- classes/Database/PDO/SQLite.php | 38 --- tests/classes/TimeTest.php | 7 + 7 files changed, 372 insertions(+), 607 deletions(-) delete mode 100644 classes/Database/Common.php delete mode 100644 classes/Database/PDO/Common.php delete mode 100644 classes/Database/PDO/MySQL.php delete mode 100644 classes/Database/PDO/PostgreSQL.php delete mode 100644 classes/Database/PDO/SQLite.php diff --git a/classes/Database.php b/classes/Database.php index dcbfbfd..ce8927a 100644 --- a/classes/Database.php +++ b/classes/Database.php @@ -16,116 +16,419 @@ */ /** - * Database Factory + * Database Class * - * Generic class to simplify connecting to a database. All database objects - * should be created by this class to future proof against any internal changes - * to PICKLES. + * Database interaction all in one place. Allows for object reuse and contains + * functions to ease interacting with databases. Common assumptions about PDO + * attributes are baked in. Only support PDO. */ class Database extends Object { /** - * Constructor + * DSN format * - * Attempts to get an instance of the passed database type or attempts to - * use a default specified in the config. - * - * @param string $name optional name of the connection to use + * @access protected + * @var string */ - public function __construct(String $name = null) - { - parent::__construct(); + protected $dsn; - return Database::getInstance($name); - } + /** + * PDO Attributes + * + * @access protected + * @var string + */ + protected $attributes = [ + PDO::ATTR_PERSISTENT => true, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::NULL_EMPTY_STRING => true, + ]; + + /** + * Driver + * + * @var string + */ + public $driver = null; + + /** + * Hostname for the server + * + * @var string + */ + public $hostname = 'localhost'; + + /** + * Port number for the server + * + * @var integer + */ + public $port = null; + + /** + * UNIX socket for the server + * + * @var integer + */ + public $socket = null; + + /** + * Username for the server + * + * @var string + */ + public $username = null; + + /** + * Password for the server + * + * @var string + */ + public $password = null; + + /** + * Database name for the server + * + * @var string + */ + public $database = null; + + /** + * Whether or not to use caching + * + * @var boolean + */ + public $cache = false; + + /** + * Connection resource + * + * @var object + */ + public $connection = null; + + /** + * Results object for the executed statement + * + * @var object + */ + public $results = null; /** * Get Instance * - * Looks up the datasource using the passed name and gets an instance of - * it. Allows for easy sharing of certain classes within the system to - * avoid the extra overhead of creating new objects each time. Also avoids - * the hassle of passing around variables (yeah I know, very global-ish) + * Instantiates a new instance of the Database class or returns the + * previously instantiated copy. * * @static - * @param string $name name of the datasource + * @param string $datasource_name name of the datasource * @return object instance of the class */ - public static function getInstance($name = null) + public static function getInstance($datasource_name = false) { $config = Config::getInstance(); - // Checks if we have a default - if ($name == null) + // Tries to load a datasource if one wasn't specified + if (!$datasource_name) { - // Checks the config for a default if (isset($config->pickles['datasource'])) { - $name = $config->pickles['datasource']; + $datasource_name = $config->pickles['datasource']; } - // Tries to use the first defined datasource elseif (is_array($config->datasources)) { - $datasources = $config->datasources; - $name = key($datasources); + $datasources = $config->datasources; + $datasource_name = key($datasources); } } - // If we have a name try to set up a connection - if ($name != null) + // Attempts to validate the datasource + if ($datasource_name) { - if (isset($config->datasources[$name])) + if (!isset(self::$instances['Database'][$datasource_name])) { - $datasource = $config->datasources[$name]; + if (!isset($config->datasources[$datasource_name])) + { + throw new Exception('The specified datasource is not defined in the config.'); + } + + $datasource = $config->datasources[$datasource_name]; if (!isset($datasource['driver'])) { - return false; + throw new Exception('The specified datasource lacks a driver.'); } $datasource['driver'] = strtolower($datasource['driver']); - if (!isset(self::$instances['Database'][$name])) + // Checks the driver is legit and scrubs the name + switch ($datasource['driver']) { - // Checks the driver is legit and scrubs the name - switch ($datasource['driver']) + case 'pdo_mysql': + $attributes = [ + 'dsn' => 'mysql:host=[[hostname]];port=[[port]];unix_socket=[[socket]];dbname=[[database]]', + 'port' => 3306, + ]; + break; + + case 'pdo_pgsql': + $attributes = [ + 'dsn' => 'pgsql:host=[[hostname]];port=[[port]];dbname=[[database]];user=[[username]];password=[[password]]', + 'port' => 5432, + ]; + break; + + case 'pdo_sqlite': + $attributes = ['dsn' => 'sqlite:[[hostname]]']; + break; + + default: + throw new Exception('Datasource driver "' . $datasource['driver'] . '" is invalid'); + break; + } + + // Instantiates our database class + $instance = new Database(); + + // Sets our database parameters + if (is_array($datasource)) + { + $datasource = array_merge($attributes, $datasource); + + foreach ($datasource as $variable => $value) { - case 'pdo_mysql': $class = 'PDO_MySQL'; break; - case 'pdo_pgsql': $class = 'PDO_PostgreSQL'; break; - case 'pdo_sqlite': $class = 'PDO_SQLite'; break; - - default: - throw new Exception('Datasource driver "' . $datasource['driver'] . '" is invalid'); - break; - } - - // Instantiates our database class - $class = 'Database_' . $class; - $instance = new $class(); - - // Sets our database parameters - if (is_array($datasource)) - { - foreach ($datasource as $variable => $value) - { - $instance->$variable = $value; - } + $instance->$variable = $value; } } // Caches the instance for possible reuse later - if (isset($instance)) - { - self::$instances['Database'][$name] = $instance; - } - - // Returns the instance - return self::$instances['Database'][$name]; + self::$instances['Database'][$datasource_name] = $instance; } + + // Returns the instance + return self::$instances['Database'][$datasource_name]; } return false; } + + /** + * Opens database connection + * + * Establishes a connection to the database based on the set configuration + * options. + * + * @return boolean true on success, throws an exception overwise + */ + public function open() + { + if ($this->connection === null) + { + // Checks that the prefix is set + if ($this->dsn == null) + { + throw new Exception('Data source name is not defined'); + } + + switch ($this->driver) + { + case 'pdo_mysql': + // Resolves "Invalid UTF-8 sequence" issues when encoding as JSON + // @todo Didn't resolve that issue, borked some other characters though + //$this->attributes[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES utf8'; + break; + + case 'pdo_pgsql': + // This combats a bug: https://bugs.php.net/bug.php?id=62571&edit=1 + $this->attributes[PDO::ATTR_PERSISTENT] = false; + + // This allows for multiple prepared queries + $this->attributes[PDO::ATTR_EMULATE_PREPARES] = true; + break; + } + + if (isset($this->username, $this->password, $this->database)) + { + // Creates a new PDO database object (persistent) + try + { + // Swaps out any variables with values in the DSN + $this->dsn = str_replace( + ['[[hostname]]', '[[port]]', '[[socket]]', '[[username]]', '[[password]]', '[[database]]'], + [$this->hostname, $this->port, $this->socket, $this->username, $this->password, $this->database], + $this->dsn + ); + + // Strips any empty parameters in the DSN + $this->dsn = str_replace(['host=;', 'port=;', 'unix_socket=;'], '', $this->dsn); + + // Attempts to establish a connection + $this->connection = new PDO($this->dsn, $this->username, $this->password, $this->attributes); + } + catch (PDOException $e) + { + throw new Exception($e); + } + } + else + { + throw new Exception('There was an error loading the database configuration'); + } + } + + return true; + } + + /** + * Closes database connection + * + * Sets the connection to null regardless of state. + * + * @return boolean always true + */ + public function close() + { + $this->connection = null; + return true; + } + + /** + * Executes an SQL Statement + * + * Executes a standard or prepared query based on passed parameters. All + * queries are logged to a file as well as timed and logged in the + * execution time is over 1 second. + * + * @param string $sql statement to execute + * @param array $input_parameters optional key/values to be bound + * @return integer ID of the last inserted row or sequence number + */ + public function execute($sql, $input_parameters = null) + { + $this->open(); + + if ($this->config->pickles['logging'] === true) + { + $loggable_query = $sql; + + if ($input_parameters != null) + { + $loggable_query .= ' -- ' . json_encode($input_parameters); + } + + Log::query($loggable_query); + } + + $sql = trim($sql); + + // Checks if the query is blank + if ($sql != '') + { + // Builds out stack trace for queries + $files = []; + + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + krsort($backtrace); + + foreach ($backtrace as $file) + { + if (isset($file['class'], $file['line'])) + { + $files[] = $file['class'] . ':' . $file['line']; + } + } + + $sql .= "\n" . '/* [' . implode('|', $files) . '] */'; + + try + { + // Establishes if we're working on an EXPLAIN + if (Profiler::enabled('explains') == true) + { + $explaining = preg_match('/^EXPLAIN /i', $sql); + $selecting = preg_match('/^SELECT /i', $sql); + } + else + { + $explaining = null; + $selecting = null; + } + + // Executes a standard query + if ($input_parameters === null) + { + // Explains the query + if ($selecting == true && $explaining == false) + { + $explain = $this->fetch('EXPLAIN ' . $sql); + } + + $start_time = microtime(true); + $this->results = $this->connection->query($sql); + } + // Executes a prepared statement + else + { + // Explains the query + if ($selecting == true && $explaining == false) + { + $explain = $this->fetch('EXPLAIN ' . $sql, $input_parameters); + } + + $start_time = microtime(true); + $this->results = $this->connection->prepare($sql); + $this->results->execute($input_parameters); + } + + $end_time = microtime(true); + $duration = $end_time - $start_time; + + if ($duration >= 1) + { + Log::slowQuery($duration . ' seconds: ' . $loggable_query); + } + + // Logs the information to the profiler + if ($explaining == false && Profiler::enabled('explains', 'queries')) + { + Profiler::logQuery($sql, $input_parameters, (isset($explain) ? $explain : false), $duration); + } + } + catch (PDOException $e) + { + throw new Exception($e); + } + } + else + { + throw new Exception('No query to execute'); + } + + return $this->connection->lastInsertId(); + } + + /** + * Fetch records from the database + * + * @param string $sql statement to be executed + * @param array $input_parameters optional key/values to be bound + * @param string $return_type optional type of return set + * @return mixed based on return type + */ + public function fetch($sql = null, $input_parameters = null) + { + $this->open(); + + if ($sql !== null) + { + $this->execute($sql, $input_parameters); + } + + // Pulls the results based on the type + $results = $this->results->fetchAll(PDO::FETCH_ASSOC); + + return $results; + } } ?> diff --git a/classes/Database/Common.php b/classes/Database/Common.php deleted file mode 100644 index 39dc726..0000000 --- a/classes/Database/Common.php +++ /dev/null @@ -1,139 +0,0 @@ - - * @copyright Copyright 2007-2014, Joshua Sherman - * @license http://www.opensource.org/licenses/mit-license.html - * @package PICKLES - * @link https://github.com/joshtronic/pickles - */ - -/** - * Common Database Abstraction Layer - * - * Parent class that our database driver classes should be extending. Contains - * basic functionality for instantiation and interfacing. - */ -abstract class Database_Common extends Object -{ - /** - * Driver - * - * @var string - */ - public $driver = null; - - /** - * Hostname for the server - * - * @var string - */ - public $hostname = 'localhost'; - - /** - * Port number for the server - * - * @var integer - */ - public $port = null; - - /** - * UNIX socket for the server - * - * @var integer - */ - public $socket = null; - - /** - * Username for the server - * - * @var string - */ - public $username = null; - - /** - * Password for the server - * - * @var string - */ - public $password = null; - - /** - * Database name for the server - * - * @var string - */ - public $database = null; - - /** - * Whether or not to use caching - * - * @var boolean - */ - public $cache = false; - - /** - * Connection resource - * - * @var object - */ - public $connection = null; - - /** - * Results object for the executed statement - * - * @var object - */ - public $results = null; - - /** - * Constructor - */ - public function __construct() - { - parent::__construct(); - - // Checks the driver is set and available - if ($this->driver == null) - { - throw new Exception('Driver name is not set'); - } - else - { - if (extension_loaded($this->driver) == false) - { - throw new Exception('Driver "' . $this->driver . '" is not loaded'); - } - } - } - - /** - * Open Database Connection - * - * Establishes a connection to the MySQL database based on the configuration - * options that are available in the Config object. - * - * @abstract - * @return boolean true on success, throws an exception overwise - */ - abstract public function open(); - - /** - * Close Database Connection - * - * Sets the connection to null regardless of state. - * - * @abstract - * @return boolean always true - */ - abstract public function close(); -} - -?> diff --git a/classes/Database/PDO/Common.php b/classes/Database/PDO/Common.php deleted file mode 100644 index cc5e125..0000000 --- a/classes/Database/PDO/Common.php +++ /dev/null @@ -1,278 +0,0 @@ - - * @copyright Copyright 2007-2014, Joshua Sherman - * @license http://www.opensource.org/licenses/mit-license.html - * @package PICKLES - * @link https://github.com/joshtronic/pickles - */ - -/** - * PDO Abstraction Layer - * - * Parent class for any of our database classes that use PDO. - */ -class Database_PDO_Common extends Database_Common -{ - /** - * DSN format - * - * @access protected - * @var string - */ - protected $dsn; - - /** - * PDO Attributes - * - * @access protected - * @var string - */ - protected $attributes = array( - PDO::ATTR_PERSISTENT => true, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::NULL_EMPTY_STRING => true, - ); - - /** - * Constructor - */ - public function __construct() - { - parent::__construct(); - - // Checks that the prefix is set - if ($this->dsn == null) - { - throw new Exception('Data source name is not defined'); - } - - switch ($this->driver) - { - case 'pdo_mysql': - // Resolves "Invalid UTF-8 sequence" issues when encoding as JSON - // @todo Didn't resolve that issue, borked some other characters though - //$this->attributes[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES utf8'; - break; - - case 'pdo_pgsql': - // This combats a bug: https://bugs.php.net/bug.php?id=62571&edit=1 - $this->attributes[PDO::ATTR_PERSISTENT] = false; - - // This allows for multiple prepared queries - $this->attributes[PDO::ATTR_EMULATE_PREPARES] = true; - - break; - } - } - - /** - * Opens database connection - * - * Establishes a connection to the database based on the set configuration - * options. - * - * @return boolean true on success, throws an exception overwise - */ - public function open() - { - if ($this->connection === null) - { - if (isset($this->username, $this->password, $this->database)) - { - // Creates a new PDO database object (persistent) - try - { - // Swaps out any variables with values in the DSN - $this->dsn = str_replace( - array('[[hostname]]', '[[port]]', '[[socket]]', '[[username]]', '[[password]]', '[[database]]'), - array($this->hostname, $this->port, $this->socket, $this->username, $this->password, $this->database), - $this->dsn - ); - - // Strips any empty parameters in the DSN - $this->dsn = str_replace(array('host=;', 'port=;', 'unix_socket=;'), '', $this->dsn); - - // Attempts to establish a connection - $this->connection = new PDO($this->dsn, $this->username, $this->password, $this->attributes); - } - catch (PDOException $e) - { - throw new Exception($e); - } - } - else - { - throw new Exception('There was an error loading the database configuration'); - } - } - - return true; - } - - /** - * Closes database connection - * - * Sets the connection to null regardless of state. - * - * @return boolean always true - */ - public function close() - { - $this->connection = null; - return true; - } - - /** - * Executes an SQL Statement - * - * Executes a standard or prepared query based on passed parameters. All - * queries are logged to a file as well as timed and logged in the - * execution time is over 1 second. - * - * @param string $sql statement to execute - * @param array $input_parameters optional key/values to be bound - * @return integer ID of the last inserted row or sequence number - */ - public function execute($sql, $input_parameters = null) - { - $this->open(); - - if ($this->config->pickles['logging'] === true) - { - $loggable_query = $sql; - - if ($input_parameters != null) - { - $loggable_query .= ' -- ' . json_encode($input_parameters); - } - - Log::query($loggable_query); - } - - $sql = trim($sql); - - // Checks if the query is blank - if ($sql != '') - { - $files = array(); - - // Ubuntu 10.04 is a bit behind on PHP 5.3.x and the IGNORE_ARGS - // constant doesn't exist. To conserve memory, the backtrace will - // Only be used on servers running PHP 5.3.6 or above. - if (version_compare(PHP_VERSION, '5.3.6', '>=')) - { - $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - krsort($backtrace); - - foreach ($backtrace as $file) - { - if (isset($file['class'], $file['line'])) - { - $files[] = $file['class'] . ':' . $file['line']; - } - } - } - - $sql .= "\n" . '/* [' . implode('|', $files) . '] */'; - - try - { - // Establishes if we're working on an EXPLAIN - if (Profiler::enabled('explains') == true) - { - $explaining = preg_match('/^EXPLAIN /i', $sql); - $selecting = preg_match('/^SELECT /i', $sql); - } - else - { - $explaining = null; - $selecting = null; - } - - // Executes a standard query - if ($input_parameters === null) - { - // Explains the query - if ($selecting == true && $explaining == false) - { - $explain = $this->fetch('EXPLAIN ' . $sql); - } - - $start_time = microtime(true); - $this->results = $this->connection->query($sql); - } - // Executes a prepared statement - else - { - // Explains the query - if ($selecting == true && $explaining == false) - { - $explain = $this->fetch('EXPLAIN ' . $sql, $input_parameters); - } - - $start_time = microtime(true); - $this->results = $this->connection->prepare($sql); - $this->results->execute($input_parameters); - } - - $end_time = microtime(true); - $duration = $end_time - $start_time; - - if ($duration >= 1) - { - Log::slowQuery($duration . ' seconds: ' . $loggable_query); - } - - // Logs the information to the profiler - if ($explaining == false && Profiler::enabled('explains', 'queries')) - { - Profiler::logQuery($sql, $input_parameters, (isset($explain) ? $explain : false), $duration); - } - } - catch (PDOException $e) - { - throw new Exception($e); - } - } - else - { - throw new Exception('No query to execute'); - } - - return $this->connection->lastInsertId(); - } - - /** - * Fetch records from the database - * - * @param string $sql statement to be executed - * @param array $input_parameters optional key/values to be bound - * @param string $return_type optional type of return set - * @return mixed based on return type - */ - public function fetch($sql = null, $input_parameters = null) - { - $this->open(); - - if ($sql !== null) - { - $this->execute($sql, $input_parameters); - } - - // Pulls the results based on the type - $results = $this->results->fetchAll(PDO::FETCH_ASSOC); - - return $results; - } -} - -?> diff --git a/classes/Database/PDO/MySQL.php b/classes/Database/PDO/MySQL.php deleted file mode 100644 index 43a1415..0000000 --- a/classes/Database/PDO/MySQL.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @copyright Copyright 2007-2014, Joshua Sherman - * @license http://www.opensource.org/licenses/mit-license.html - * @package PICKLES - * @link https://github.com/joshtronic/pickles - */ - -/** - * MySQL Database Abstraction Layer - */ -class Database_PDO_MySQL extends Database_PDO_Common -{ - /** - * Driver - * - * @var string - */ - public $driver = 'pdo_mysql'; - - /** - * DSN format - * - * @var string - */ - public $dsn = 'mysql:host=[[hostname]];port=[[port]];unix_socket=[[socket]];dbname=[[database]]'; - - /** - * Default port - * - * @var integer - */ - public $port = 3306; -} - -?> diff --git a/classes/Database/PDO/PostgreSQL.php b/classes/Database/PDO/PostgreSQL.php deleted file mode 100644 index 68579ce..0000000 --- a/classes/Database/PDO/PostgreSQL.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @copyright Copyright 2007-2014, Joshua Sherman - * @license http://www.opensource.org/licenses/mit-license.html - * @package PICKLES - * @link https://github.com/joshtronic/pickles - */ - -/** - * PostgreSQL Database Abstraction Layer - */ -class Database_PDO_PostgreSQL extends Database_PDO_Common -{ - /** - * Driver - * - * @var string - */ - public $driver = 'pdo_pgsql'; - - /** - * DSN format - * - * @var string - */ - public $dsn = 'pgsql:host=[[hostname]];port=[[port]];dbname=[[database]];user=[[username]];password=[[password]]'; - - /** - * Default port - * - * @var integer - */ - public $port = 5432; -} - -?> diff --git a/classes/Database/PDO/SQLite.php b/classes/Database/PDO/SQLite.php deleted file mode 100644 index d8ed2db..0000000 --- a/classes/Database/PDO/SQLite.php +++ /dev/null @@ -1,38 +0,0 @@ - - * @copyright Copyright 2007-2014, Joshua Sherman - * @license http://www.opensource.org/licenses/mit-license.html - * @package PICKLES - * @link https://github.com/joshtronic/pickles - */ - -/** - * SQLite Database Abstraction Layer - */ -class Database_PDO_SQLite extends Database_PDO_Common -{ - /** - * Driver - * - * @var string - */ - public $driver = 'pdo_sqlite'; - - /** - * DSN format - * - * @var string - */ - public $dsn = 'sqlite:[[hostname]]'; -} - -?> diff --git a/tests/classes/TimeTest.php b/tests/classes/TimeTest.php index d41ef34..3dc2ce6 100644 --- a/tests/classes/TimeTest.php +++ b/tests/classes/TimeTest.php @@ -47,6 +47,13 @@ class TimeTest extends PHPUnit_Framework_TestCase $this->assertEquals('1 day ago', Time::ago(strtotime('-1 day'))); } + /* @todo Need to fix these results so it doesn't fail. + public function testAgoPastTimeDays2() + { + $this->assertEquals('1 day ago', Time::ago(strtotime('-23 hours -55 minutes'))); + } + */ + public function testAgoPastTimeWeeks() { $this->assertEquals('1 week ago', Time::ago(strtotime('-1 week')));