Skip to content

Instantly share code, notes, and snippets.

@brendo
Last active December 16, 2015 13:19
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brendo/5440653 to your computer and use it in GitHub Desktop.
Save brendo/5440653 to your computer and use it in GitHub Desktop.
Improved Symphony MySQL Driver - Uses `mysqli` instead of `mysql`. - Supports a `master`, `slave` setup where the master accepts all writes, and the slaves processes reads To use, add an additional section to your config, `database_slave` which has the same details as `database`.
<?php
/**
* @package toolkit
*/
/**
* The DatabaseException class extends a normal Exception to add in
* debugging information when a SQL query fails such as the internal
* database error code and message in additional to the usual
* Exception information. It allows a DatabaseException to contain a human
* readable error, as well more technical information for debugging.
*/
Class DatabaseException extends Exception{
/**
* An associative array with three keys, 'query', 'msg' and 'num'
* @var array
*/
private $_error = array();
/**
* Constructor takes a message and an associative array to set to
* `$_error`. The message is passed to the default Exception constructor
*/
public function __construct($message, array $error=NULL){
parent::__construct($message);
$this->_error = $error;
}
/**
* Accessor function for the original query that caused this Exception
*
* @return string
*/
public function getQuery(){
return $this->_error['query'];
}
/**
* Accessor function for the Database error code for this type of error
*
* @return string
*/
public function getDatabaseErrorCode(){
return $this->_error['num'];
}
/**
* Accessor function for the Database message from this Exception
*
* @return string
*/
public function getDatabaseErrorMessage(){
return $this->_error['msg'];
}
}
/**
* The MySQL class acts as a wrapper for connecting to the Database
* in Symphony. It utilises mysql_* functions in PHP to complete the usual
* querying. As well as the normal set of insert, update, delete and query
* functions, some convenience functions are provided to return results
* in different ways. Symphony uses a prefix to namespace it's tables in a
* database, allowing it play nice with other applications installed on the
* database. An errors that occur during a query throw a `DatabaseException`.
* By default, Symphony logs all queries to be used for Profiling and Debug
* devkit extensions when a Developer is logged in. When a developer is not
* logged in, all queries and errors are made available with delegates.
*/
Class MySQL {
/**
* Constant to indicate whether the query is a write operation.
*
* @var integer
*/
const __WRITE_OPERATION__ = 0;
/**
* Constant to indicate whether the query is a write operation
*
* @var integer
*/
const __READ_OPERATION__ = 1;
/**
* Sets the current `$_log` to be an empty array
*
* @var array
*/
private static $_log = array();
/**
* The number of queries this class has executed, defaults to 0.
*
* @var integer
*/
private static $_query_count = 0;
/**
* Whether query caching is enabled or not. By default this set
* to true which will use SQL_CACHE to cache the results of queries
*
* @var boolean
*/
private static $_cache = true;
/**
* An associative array of connection properties for this MySQL
* database including the host, port, username, password and
* selected database.
*
* @var array
*/
private static $_connection = array();
/**
* The resource of the last result returned from mysql_query
*
* @var resource
*/
private $_result = null;
/**
* The last query that was executed by the class
*/
private $_lastQuery = null;
/**
* The hash value of the last query that was executed by the class
*/
private $_lastQueryHash = null;
/**
* The auto increment value returned by the last query that was executed
* by the class
*/
private $_lastInsertID = null;
/**
* By default, an array of arrays or objects representing the result set
* from the `$this->_lastQuery`
*/
private $_lastResult = array();
/**
* Magic function that will flush the MySQL log and close the MySQL
* connection when the MySQL class is removed or destroyed.
*
* @link http://php.net/manual/en/language.oop5.decon.php
*/
public function __destruct(){
$this->flush();
$this->close();
}
/**
* Resets the result, `$this->_lastResult` and `$this->_lastQuery` to their empty
* values. Called on each query and when the class is destroyed.
*/
public function flush(){
$this->_result = null;
$this->_lastResult = array();
$this->_lastQuery = null;
$this->_lastQueryHash = null;
}
/**
* Sets the current `$_log` to be an empty array
*/
public static function flushLog(){
self::$_log = array();
}
/**
* Returns the number of queries that has been executed
*
* @return integer
*/
public static function queryCount(){
return self::$_query_count;
}
/**
* Sets query caching to true, this will prepend all READ_OPERATION
* queries with SQL_CACHE. Symphony be default enables caching. It
* can be turned off by setting the query_cache parameter to 'off' in the
* Symphony config file.
*
* @link http://dev.mysql.com/doc/refman/5.1/en/query-cache.html
*/
public static function enableCaching(){
MySQL::$_cache = true;
}
/**
* Sets query caching to false, this will prepend all READ_OPERATION
* queries will SQL_NO_CACHE.
*/
public static function disableCaching(){
MySQL::$_cache = false;
}
/**
* Returns boolean if query caching is enabled or not
*
* @return boolean
*/
public static function isCachingEnabled(){
return MySQL::$_cache;
}
/**
* Symphony uses a prefix for all it's database tables so it can live peacefully
* on the same database as other applications. By default this is sym_, but it
* can be changed when Symphony is installed.
*
* @param string $prefix
* The table prefix for Symphony, by default this is sym_
* @param string $type
* The type of Database connection to use. Useful in load balanced
* DB environments. Defaults to 'master'.
*/
public function setPrefix($prefix, $type = 'master'){
MySQL::$_connection[$type]['tbl_prefix'] = $prefix;
}
/**
* Determines if a connection has been made to the MySQL server
*
* @param string $type
* The type of Database connection to use. Useful in load balanced
* DB environments. Defaults to 'master'.
* @return boolean
*/
public function isConnected($type = 'master'){
return (isset(MySQL::$_connection[$type]['id']) && !is_null(MySQL::$_connection[$type]['id']));
}
/**
* Called when the script has finished executing, this closes the MySQL
* connection
*
* @return boolean
*/
public function close(){
if($this->isConnected()) {
if(isset(MySQL::$_connection['slave'])) {
mysqli_close(MySQL::$_connection['slave']['id']);
}
return mysqli_close(MySQL::$_connection['master']['id']);
}
}
/**
* Creates a connect to the database server given the credentials. If an
* error occurs, a `DatabaseException` is thrown, otherwise true is returned
*
* @param string $host
* Defaults to null, which MySQL assumes as localhost.
* @param string $user
* Defaults to null
* @param string $password
* Defaults to null
* @param string $port
* Defaults to 3306.
* @param string $database
* The database to connect to upon connection
* @param string $type
* The type of Database connection to use. Useful in load balanced
* DB environments. Defaults to 'master'.
* @return boolean
*/
public function connect($host = null, $user = null, $password = null, $port ='3306', $database = null, $type = 'master') {
MySQL::$_connection[$type] = array(
'host' => $host,
'user' => $user,
'pass' => $password,
'port' => $port,
'database' => $database
);
try {
MySQL::$_connection[$type]['id'] = mysqli_connect(
MySQL::$_connection[$type]['host'], MySQL::$_connection[$type]['user'], MySQL::$_connection[$type]['pass'], MySQL::$_connection[$type]['database'], MySQL::$_connection[$type]['port']
);
if(!$this->isConnected($type)) {
$this->__error($type, 'connect');
}
}
catch (Exception $ex) {
$this->__error($type, 'connect');
}
return true;
}
/**
* Accessor for the current MySQL resource from PHP. May be
* useful for developers who want complete control over their
* database queries and don't want anything abstract by the MySQL
* class.
*
* @param string $type
* The type of Database connection to use. Useful in load balanced
* DB environments. Defaults to 'master'.
* @return resource
*/
public static function getConnectionResource($type = 'master') {
return MySQL::$_connection[$type]['id'];
}
/**
* This will set the character encoding of the connection for sending and
* receiving data. This function will run every time the database class
* is being initialized. If no character encoding is provided, UTF-8
* is assumed.
*
* @link http://au2.php.net/manual/en/function.mysql-set-charset.php
* @param string $set
* The character encoding to use, by default this 'utf8'
* @param string $type
* The type of Database connection to use. Useful in load balanced
* DB environments. Defaults to 'master'.
*/
public function setCharacterEncoding($set='utf8', $type = 'master'){
mysqli_set_charset(MySQL::$_connection[$type]['id'], $set);
}
/**
* This function will set the character encoding of the database so that any
* new tables that are created by Symphony use this character encoding
*
* @link http://dev.mysql.com/doc/refman/5.0/en/charset-connection.html
* @param string $set
* The character encoding to use, by default this 'utf8'
*/
public function setCharacterSet($set='utf8') {
$this->query("SET character_set_connection = '$set', character_set_database = '$set', character_set_server = '$set'");
$this->query("SET CHARACTER SET '$set'");
}
/**
* This function will clean a string using the `mysql_real_escape_string` function
* taking into account the current database character encoding. Note that this
* function does not encode _ or %. If `mysql_real_escape_string` doesn't exist,
* `addslashes` will be used as a backup option
*
* @param string $value
* The string to be encoded into an escaped SQL string
* @return string
* The escaped SQL string
*/
public static function cleanValue($value) {
if (function_exists('mysql_real_escape_string')) {
return mysqli_real_escape_string(MySQL::$_connection['master']['id'], $value);
} else {
return addslashes($value);
}
}
/**
* This function will apply the `cleanValue` function to an associative
* array of data, encoding only the value, not the key. This function
* can handle recursive arrays. This function manipulates the given
* parameter by reference.
*
* @see cleanValue
* @param array $array
* The associative array of data to encode, this parameter is manipulated
* by reference.
*/
public static function cleanFields(array &$array){
foreach($array as $key => $val){
// Handle arrays with more than 1 level
if(is_array($val)){
self::cleanFields($val);
continue;
}
elseif(strlen($val) == 0){
$array[$key] = 'NULL';
}
else{
$array[$key] = "'" . self::cleanValue($val) . "'";
}
}
}
/**
* Determines whether this query is a read operation, or if it is a write operation.
* A write operation is determined as any query that starts with CREATE, INSERT,
* REPLACE, ALTER, DELETE, UPDATE, OPTIMIZE or TRUNCATE. Anything else is
* considered to be a read operation which are subject to query caching.
*
* @return integer
* `MySQL::__WRITE_OPERATION__` or `MySQL::__READ_OPERATION__`
*/
public function determineQueryType($query){
return (preg_match('/^(create|insert|replace|alter|delete|update|optimize|truncate|drop)/i', $query) ? MySQL::__WRITE_OPERATION__ : MySQL::__READ_OPERATION__);
}
/**
* Takes an SQL string and executes it. This function will apply query
* caching if it is a read operation and if query caching is set. Symphony
* will convert the `tbl_` prefix of tables to be the one set during installation.
* A type parameter is provided to specify whether `$this->_lastResult` will be an array
* of objects or an array of associative arrays. The default is objects. This
* function will return boolean, but set `$this->_lastResult` to the result.
*
* @uses PostQueryExecution
* @param string $query
* The full SQL query to execute.
* @param string $type
* Whether to return the result as objects or associative array. Defaults
* to OBJECT which will return objects. The other option is ASSOC. If $type
* is not either of these, it will return objects.
* @return boolean
* True if the query executed without errors, false otherwise
*/
public function query($query, $type = "OBJECT"){
if(empty($query)) return false;
$start = precision_timer();
$query = trim($query);
$query_type = $this->determineQueryType($query);
$query_hash = md5($query.$start);
$db_type = 'master';
if(MySQL::$_connection['master']['tbl_prefix'] != 'tbl_'){
$query = preg_replace('/tbl_(\S+?)([\s\.,]|$)/', MySQL::$_connection['master']['tbl_prefix'].'\\1\\2', $query);
}
// TYPE is deprecated since MySQL 4.0.18, ENGINE is preferred
if($query_type == MySQL::__WRITE_OPERATION__) {
$query = preg_replace('/TYPE=(MyISAM|InnoDB)/i', 'ENGINE=$1', $query);
}
else if($query_type == MySQL::__READ_OPERATION__ && !preg_match('/^SELECT\s+SQL(_NO)?_CACHE/i', $query)){
if(isset(MySQL::$_connection['slave'])) {
$db_type = 'slave';
}
if($this->isCachingEnabled()) {
$query = preg_replace('/^SELECT\s+/i', 'SELECT SQL_CACHE ', $query);
}
else {
$query = preg_replace('/^SELECT\s+/i', 'SELECT SQL_NO_CACHE ', $query);
}
}
$this->flush();
$this->_lastQuery = $query;
$this->_lastQueryHash = $query_hash;
$this->_result = mysqli_query(MySQL::$_connection[$db_type]['id'], $query);
$this->_lastInsertID = mysqli_insert_id(MySQL::$_connection[$db_type]['id']);
self::$_query_count++;
if(mysqli_error(MySQL::$_connection[$db_type]['id'])){
$this->__error($db_type);
}
else if(($this->_result instanceof mysqli_result)){
if($type == "ASSOC") {
while ($row = mysqli_fetch_assoc($this->_result)){
$this->_lastResult[] = $row;
}
}
else {
while ($row = mysqli_fetch_object($this->_result)){
$this->_lastResult[] = $row;
}
}
mysqli_free_result($this->_result);
}
$stop = precision_timer('stop', $start);
/**
* After a query has successfully executed, that is it was considered
* valid SQL, this delegate will provide the query, the query_hash and
* the execution time of the query.
*
* Note that this function only starts logging once the ExtensionManager
* is available, which means it will not fire for the first couple of
* queries that set the character set.
*
* @since Symphony 2.3
* @delegate PostQueryExecution
* @param string $context
* '/frontend/' or '/backend/'
* @param string $query
* The query that has just been executed
* @param string $query_hash
* The hash used by Symphony to uniquely identify this query
* @param float $execution_time
* The time that it took to run `$query`
*/
if(Symphony::ExtensionManager() instanceof ExtensionManager) {
Symphony::ExtensionManager()->notifyMembers('PostQueryExecution', class_exists('Administration') ? '/backend/' : '/frontend/', array(
'query' => '[' . $db_type . '] ' . $query,
'query_hash' => $query_hash,
'execution_time' => $stop
));
// If the ExceptionHandler is enabled, then the user is authenticated
// or we have a serious issue, so log the query.
if(GenericExceptionHandler::$enabled) {
self::$_log[$query_hash] = array(
'query' => '[' . $db_type . '] ' . $query,
'query_hash' => $query_hash,
'execution_time' => $stop
);
}
}
// Symphony isn't ready yet. Log internally
else {
self::$_log[$query_hash] = array(
'query' => '[' . $db_type . '] ' . $query,
'query_hash' => $query_hash,
'execution_time' => $stop
);
}
return true;
}
/**
* Returns the last insert ID from the previous query. This is
* the value from an auto_increment field.
*
* @return integer
* The last interested row's ID
*/
public function getInsertID(){
return $this->_lastInsertID;
}
/**
* A convenience method to insert data into the Database. This function
* takes an associative array of data to input, with the keys being the column
* names and the table. An optional parameter exposes MySQL's ON DUPLICATE
* KEY UPDATE functionality, which will update the values if a duplicate key
* is found.
*
* @param array $fields
* An associative array of data to input, with the key's mapping to the
* column names. Alternatively, an array of associative array's can be
* provided, which will perform multiple inserts
* @param string $table
* The table name, including the tbl prefix which will be changed
* to this Symphony's table prefix in the query function
* @param boolean $updateOnDuplicate
* If set to true, data will updated if any key constraints are found that cause
* conflicts. By default this is set to false, which will not update the data and
* would return an SQL error
* @return boolean
*/
public function insert(array $fields, $table, $updateOnDuplicate=false){
// Multiple Insert
if(is_array(current($fields))){
$sql = "INSERT INTO `$table` (`".implode('`, `', array_keys(current($fields))).'`) VALUES ';
foreach($fields as $key => $array){
// Sanity check: Make sure we dont end up with ',()' in the SQL.
if(!is_array($array)) continue;
self::cleanFields($array);
$rows[] = '('.implode(', ', $array).')';
}
$sql .= implode(", ", $rows);
}
// Single Insert
else{
self::cleanFields($fields);
$sql = "INSERT INTO `$table` (`".implode('`, `', array_keys($fields)).'`) VALUES ('.implode(', ', $fields).')';
if($updateOnDuplicate){
$sql .= ' ON DUPLICATE KEY UPDATE ';
foreach($fields as $key => $value) $sql .= " `$key` = $value,";
$sql = trim($sql, ',');
}
}
return $this->query($sql);
}
/**
* A convenience method to update data that exists in the Database. This function
* takes an associative array of data to input, with the keys being the column
* names and the table. A WHERE statement can be provided to select the rows
* to update
*
* @param array $fields
* An associative array of data to input, with the key's mapping to the
* column names.
* @param string $table
* The table name, including the tbl prefix which will be changed
* to this Symphony's table prefix in the query function
* @param string $where
* A WHERE statement for this UPDATE statement, defaults to null
* which will update all rows in the $table
* @return boolean
*/
public function update($fields, $table, $where = null) {
self::cleanFields($fields);
$sql = "UPDATE $table SET ";
foreach($fields as $key => $val)
$rows[] = " `$key` = $val";
$sql .= implode(', ', $rows) . (!is_null($where) ? ' WHERE ' . $where : null);
return $this->query($sql);
}
/**
* Given a table name and a WHERE statement, delete rows from the
* Database.
*
* @param string $table
* The table name, including the tbl prefix which will be changed
* to this Symphony's table prefix in the query function
* @param string $where
* A WHERE statement for this DELETE statement, defaults to null,
* which will delete all rows in the $table
* @return boolean
*/
public function delete($table, $where = null){
return $this->query("DELETE FROM $table WHERE $where");
}
/**
* Returns an associative array that contains the results of the
* given `$query`. Optionally, the resulting array can be indexed
* by a particular column.
*
* @param string $query
* The full SQL query to execute. Defaults to null, which will
* use the _lastResult
* @param string $index_by_column
* The name of a column in the table to use it's value to index
* the result by. If this is omitted (and it is by default), an
* array of associative arrays is returned, with the key being the
* column names
* @return array
* An associative array with the column names as the keys
*/
public function fetch($query = null, $index_by_column = null){
if(!is_null($query)) {
$this->query($query, "ASSOC");
}
else if(is_null($this->_lastResult)) {
return array();
}
$result = $this->_lastResult;
if(!is_null($index_by_column) && isset($result[0][$index_by_column])){
$n = array();
foreach($result as $ii) {
$n[$ii[$index_by_column]] = $ii;
}
$result = $n;
}
return $result;
}
/**
* Returns the row at the specified index from the given query. If no
* query is given, it will use the `$this->_lastResult`. If no offset is provided,
* the function will return the first row. This function does not imply any
* LIMIT to the given `$query`, so for the more efficient use, it is recommended
* that the `$query` have a LIMIT set.
*
* @param integer $offset
* The row to return from the SQL query. For instance, if the second
* row from the result was required, the offset would be 1, because it
* is zero based.
* @param string $query
* The full SQL query to execute. Defaults to null, which will
* use the `$this->_lastResult`
* @return array
* If there is no row at the specified `$offset`, an empty array will be returned
* otherwise an associative array of that row will be returned.
*/
public function fetchRow($offset = 0, $query = null){
$result = $this->fetch($query);
return (empty($result) ? array() : $result[$offset]);
}
/**
* Returns an array of values for a specified column in a given query.
* If no query is given, it will use the `$this->_lastResult`.
*
* @param string $column
* The column name in the query to return the values for
* @param string $query
* The full SQL query to execute. Defaults to null, which will
* use the `$this->_lastResult`
* @return array
* If there is no results for the `$query`, an empty array will be returned
* otherwise an array of values for that given `$column` will be returned
*/
public function fetchCol($column, $query = null){
$result = $this->fetch($query);
if(empty($result)) return array();
foreach ($result as $row){
$return[] = $row[$column];
}
return $return;
}
/**
* Returns the value for a specified column at a specified offset. If no
* offset is provided, it will return the value for column of the first row.
* If no query is given, it will use the `$this->_lastResult`.
*
* @param string $column
* The column name in the query to return the values for
* @param integer $offset
* The row to use to return the value for the given `$column` from the SQL
* query. For instance, if `$column` form the second row was required, the
* offset would be 1, because it is zero based.
* @param string $query
* The full SQL query to execute. Defaults to null, which will
* use the `$this->_lastResult`
* @return string|null
* Returns the value of the given column, if it doesn't exist, null will be
* returned
*/
public function fetchVar($column, $offset = 0, $query = null){
$result = $this->fetch($query);
return (empty($result) ? null : $result[$offset][$column]);
}
/**
* This function takes `$table` and `$field` names and returns boolean
* if the `$table` contains the `$field`.
*
* @since Symphony 2.3
* @param string $table
* The table name
* @param string $field
* The field name
* @return boolean
* True if `$table` contains `$field`, false otherwise
*/
public function tableContainsField($table, $field){
$results = $this->fetch("DESC `{$table}` `{$field}`");
return (is_array($results) && !empty($results));
}
/**
* If an error occurs in a query, this function is called which logs
* the last query and the error number and error message from MySQL
* before throwing a `DatabaseException`
*
* @uses QueryExecutionError
* @throws DatabaseException
*/
private function __error($type = 'master', $state = null) {
if($state == 'connect') {
$msg = mysqli_connect_error();
$errornum = mysqli_connect_errno();
}
else {
$msg = mysqli_error(MySQL::$_connection[$type]['id']);
$errornum = mysqli_errno(MySQL::$_connection[$type]['id']);
}
/**
* After a query execution has failed this delegate will provide the query,
* query hash, error message and the error number.
*
* Note that this function only starts logging once the `ExtensionManager`
* is available, which means it will not fire for the first couple of
* queries that set the character set.
*
* @since Symphony 2.3
* @delegate QueryExecutionError
* @param string $context
* '/frontend/' or '/backend/'
* @param string $query
* The query that has just been executed
* @param string $query_hash
* The hash used by Symphony to uniquely identify this query
* @param string $msg
* The error message provided by MySQL which includes information on why the execution failed
* @param integer $num
* The error number that corresponds with the MySQL error message
*/
if(Symphony::ExtensionManager() instanceof ExtensionManager) {
Symphony::ExtensionManager()->notifyMembers('QueryExecutionError', class_exists('Administration') ? '/backend/' : '/frontend/', array(
'query' => $this->_lastQuery,
'query_hash' => $this->_lastQueryHash,
'msg' => $msg,
'num' => $errornum
));
}
throw new DatabaseException(__('MySQL Error (%1$s): %2$s in query: %3$s', array($errornum, $msg, $this->_lastQuery)), array(
'msg' => $msg,
'num' => $errornum,
'query' => $this->_lastQuery
));
}
/**
* Returns all the log entries by type. There are two valid types,
* error and debug. If no type is given, the entire log is returned,
* otherwise only log messages for that type are returned
*
* @return array
* An array of associative array's. Log entries of the error type
* return the query the error occurred on and the error number and
* message from MySQL. Log entries of the debug type return the
* the query and the start/stop time to indicate how long it took
* to run
*/
public function debug($type = null){
if(!$type) return self::$_log;
return ($type == 'error' ? self::$_log['error'] : self::$_log['query']);
}
/**
* Returns some basic statistics from the MySQL class about the
* number of queries, the time it took to query and any slow queries.
* A slow query is defined as one that took longer than 0.0999 seconds
* This function is used by the Profile devkit
*
* @return array
* An associative array with the number of queries, an array of slow
* queries and the total query time.
*/
public function getStatistics() {
$stats = array();
$query_timer = 0.0;
$slow_queries = array();
foreach(self::$_log as $key => $val) {
$query_timer += $val['execution_time'];
if($val['execution_time'] > 0.0999) $slow_queries[] = $val;
}
return array(
'queries' => MySQL::queryCount(),
'slow-queries' => $slow_queries,
'total-query-time' => number_format($query_timer, 4, '.', '')
);
}
/**
* Convenience function to allow you to execute multiple SQL queries at once
* by providing a string with the queries delimited with a `;`
*
* @param string $sql
* A string containing SQL queries delimited by `;`
* @param boolean $force_engine
* If set to true, this will set MySQL's default storage engine to MyISAM.
* Defaults to false, which will use MySQL's default storage engine when
* tables don't explicitly define which engine they should be created with
* @return boolean
* If one of the queries fails, false will be returned and no further queries
* will be executed, otherwise true will be returned.
*/
public function import($sql, $force_engine = false){
if($force_engine){
// Silently attempt to change the storage engine. This prevents INNOdb errors.
$this->query('SET storage_engine=MYISAM');
}
$queries = preg_split('/;[\\r\\n]+/', $sql, -1, PREG_SPLIT_NO_EMPTY);
if(!is_array($queries) || empty($queries) || count($queries) <= 0){
throw new Exception('The SQL string contains no queries.');
}
foreach($queries as $sql){
if(trim($sql) != '') $result = $this->query($sql);
if(!$result) return false;
}
return true;
}
}
<?php
/**
* @package core
*/
/**
* The Symphony class is an abstract class that implements the
* Singleton interface. It provides the glue that forms the Symphony
* CMS and initialises the toolkit classes. Symphony is extended by
* the Frontend and Administration classes
*/
require_once(CORE . '/class.errorhandler.php');
require_once(CORE . '/class.configuration.php');
require_once(CORE . '/class.log.php');
require_once(CORE . '/class.cookie.php');
require_once(CORE . '/interface.singleton.php');
require_once(TOOLKIT . '/class.page.php');
require_once(TOOLKIT . '/class.ajaxpage.php');
require_once(TOOLKIT . '/class.xmlelement.php');
require_once(TOOLKIT . '/class.widget.php');
require_once(TOOLKIT . '/class.general.php');
require_once(TOOLKIT . '/class.cryptography.php');
require_once(TOOLKIT . '/class.profiler.php');
require_once(TOOLKIT . '/class.author.php');
require_once(TOOLKIT . '/class.email.php');
require_once(TOOLKIT . '/class.mysql.php');
require_once(TOOLKIT . '/class.extensionmanager.php');
require_once(TOOLKIT . '/class.pagemanager.php');
require_once(TOOLKIT . '/class.authormanager.php');
require_once(TOOLKIT . '/class.emailgatewaymanager.php');
require_once(TOOLKIT . '/class.entrymanager.php');
require_once(TOOLKIT . '/class.fieldmanager.php');
require_once(TOOLKIT . '/class.sectionmanager.php');
require_once(TOOLKIT . '/class.textformattermanager.php');
require_once(TOOLKIT . '/class.datasourcemanager.php');
require_once(TOOLKIT . '/class.eventmanager.php');
Abstract Class Symphony implements Singleton{
/**
* An instance of the Symphony class, either `Administration` or `Frontend`.
* @var Symphony
*/
protected static $_instance = null;
/**
* An instance of the `Configuration` class
* @var Configuration
*/
private static $Configuration = null;
/**
* An instance of the `Database` class
* @var MySQL
*/
private static $Database = null;
/**
* An instance of the `ExtensionManager` class
* @var ExtensionManager
*/
private static $ExtensionManager = null;
/**
* An instance of the `Log` class
* @var Log
*/
private static $Log = null;
/**
* An instance of the Profiler class
* @var Profiler
*/
private static $Profiler = null;
/**
* The current page namespace, used for translations
* @since Symphony 2.3
* @var string
*/
private static $namespace = false;
/**
* A previous exception that has been fired. Defaults to null.
* @since Symphony 2.3.2
* @var Exception
*/
private $exception = null;
/**
* An instance of the Cookie class
* @var Cookie
*/
public $Cookie = null;
/**
* An instance of the currently logged in Author
* @var Author
*/
public $Author = null;
/**
* The Symphony constructor initialises the class variables of Symphony.
* It will set the DateTime settings, define new date constants and initialise
* the correct Language for the currently logged in Author. If magic quotes
* are enabled, Symphony will sanitize the `$_SERVER`, `$_COOKIE`,
* `$_GET` and `$_POST` arrays. The constructor loads in
* the initial Configuration values from the `CONFIG` file
*/
protected function __construct(){
self::$Profiler = Profiler::instance();
self::$Profiler->sample('Engine Initialisation');
if(get_magic_quotes_gpc()) {
General::cleanArray($_SERVER);
General::cleanArray($_COOKIE);
General::cleanArray($_GET);
General::cleanArray($_POST);
}
$this->initialiseConfiguration();
define_safe('__SYM_DATE_FORMAT__', self::Configuration()->get('date_format', 'region'));
define_safe('__SYM_TIME_FORMAT__', self::Configuration()->get('time_format', 'region'));
define_safe('__SYM_DATETIME_FORMAT__', __SYM_DATE_FORMAT__ . self::Configuration()->get('datetime_separator', 'region') . __SYM_TIME_FORMAT__);
DateTimeObj::setSettings(self::Configuration()->get('region'));
// Initialize language management
Lang::initialize();
$this->initialiseLog();
GenericExceptionHandler::initialise(self::Log());
GenericErrorHandler::initialise(self::Log());
$this->initialiseDatabase();
$this->initialiseExtensionManager();
$this->initialiseCookie();
// If the user is not a logged in Author, turn off the verbose error messages.
if(!self::isLoggedIn() && is_null($this->Author)){
GenericExceptionHandler::$enabled = false;
}
// Set system language
Lang::set(self::$Configuration->get('lang', 'symphony'));
}
/**
* Accessor for the Symphony instance, whether it be Frontend
* or Administration
*
* @since Symphony 2.2
* @return Symphony
*/
public static function Engine() {
if(class_exists('Administration')) {
return Administration::instance();
}
else if(class_exists('Frontend')) {
return Frontend::instance();
}
else throw new Exception(__('No suitable engine object found'));
}
/**
* Setter for `$Configuration`. This function initialise the configuration
* object and populate its properties based on the given $array.
*
* @since Symphony 2.3
* @param array $data
* An array of settings to be stored into the Configuration object
*/
public function initialiseConfiguration(array $data = array()){
if(empty($data)){
// Includes the existing CONFIG file and initialises the Configuration
// by setting the values with the setArray function.
include(CONFIG);
$data = $settings;
}
self::$Configuration = new Configuration(true);
self::$Configuration->setArray($data);
}
/**
* Accessor for the current `Configuration` instance. This contains
* representation of the the Symphony config file.
*
* @return Configuration
*/
public static function Configuration(){
return self::$Configuration;
}
/**
* Accessor for the current `Profiler` instance.
*
* @since Symphony 2.3
* @return Profiler
*/
public static function Profiler(){
return self::$Profiler;
}
/**
* Setter for `$Log`. This function uses the configuration
* settings in the 'log' group in the Configuration to create an instance. Date
* formatting options are also retrieved from the configuration.
*
* @param string $filename (optional)
* The file to write the log to, if omitted this will default to `ACTIVITY_LOG`
*/
public function initialiseLog($filename = null) {
if(self::$Log instanceof Log && self::$Log->getLogPath() == $filename) return true;
if(is_null($filename)) $filename = ACTIVITY_LOG;
self::$Log = new Log($filename);
self::$Log->setArchive((self::Configuration()->get('archive', 'log') == '1' ? true : false));
self::$Log->setMaxSize(intval(self::Configuration()->get('maxsize', 'log')));
self::$Log->setDateTimeFormat(self::Configuration()->get('date_format', 'region') . ' ' . self::Configuration()->get('time_format', 'region'));
if(self::$Log->open(Log::APPEND, self::Configuration()->get('write_mode', 'file')) == 1){
self::$Log->initialise('Symphony Log');
}
}
/**
* Accessor for the current `Log` instance
*
* @since Symphony 2.3
* @return Log
*/
public static function Log() {
return self::$Log;
}
/**
* Setter for `$Cookie`. This will use PHP's parse_url
* function on the current URL to set a cookie using the cookie_prefix
* defined in the Symphony configuration. The cookie will last two
* weeks.
*
* This function also defines two constants, `__SYM_COOKIE_PATH__`
* and `__SYM_COOKIE_PREFIX__`.
*
* @deprecated Prior to Symphony 2.3.2, the constant `__SYM_COOKIE_PREFIX_`
* had a typo where it was missing the second underscore. Symphony will
* support both constants, `__SYM_COOKIE_PREFIX_` and `__SYM_COOKIE_PREFIX__`
* until Symphony 2.5
*/
public function initialiseCookie(){
$cookie_path = @parse_url(URL, PHP_URL_PATH);
$cookie_path = '/' . trim($cookie_path, '/');
define_safe('__SYM_COOKIE_PATH__', $cookie_path);
define_safe('__SYM_COOKIE_PREFIX_', self::Configuration()->get('cookie_prefix', 'symphony'));
define_safe('__SYM_COOKIE_PREFIX__', self::Configuration()->get('cookie_prefix', 'symphony'));
$this->Cookie = new Cookie(__SYM_COOKIE_PREFIX__, TWO_WEEKS, __SYM_COOKIE_PATH__);
}
/**
* Setter for `$ExtensionManager` using the current
* Symphony instance as the parent. If for some reason this fails,
* a Symphony Error page will be thrown
*/
public function initialiseExtensionManager(){
if(self::$ExtensionManager instanceof ExtensionManager) return true;
self::$ExtensionManager = new ExtensionManager;
if(!(self::$ExtensionManager instanceof ExtensionManager)){
$this->throwCustomError(__('Error creating Symphony extension manager.'));
}
}
/**
* Accessor for the current `$ExtensionManager` instance.
*
* @since Symphony 2.2
* @return ExtensionManager
*/
public static function ExtensionManager() {
return self::$ExtensionManager;
}
/**
* Setter for `$Database`, accepts a Database object. If `$database`
* is omitted, this function will set `$Database` to be of the `MySQL`
* class.
*
* @since Symphony 2.3
* @param StdClass $database (optional)
* The class to handle all Database operations, if omitted this function
* will set `self::$Database` to be an instance of the `MySQL` class.
* @return boolean
* This function will always return true
*/
public function setDatabase(StdClass $database = null) {
if (self::Database()) return true;
self::$Database = !is_null($database) ? $database : new MySQL;
return true;
}
/**
* Accessor for the current `$Database` instance.
*
* @return MySQL
*/
public static function Database(){
return self::$Database;
}
/**
* This will initialise the Database class and attempt to create a connection
* using the connection details provided in the Symphony configuration. If any
* errors occur whilst doing so, a Symphony Error Page is displayed.
*
* @return boolean
* This function will return true if the `$Database` was
* initialised successfully.
*/
public function initialiseDatabase(){
$this->setDatabase();
$details = self::Configuration()->get('database');
try{
if(!self::Database()->connect($details['host'], $details['user'], $details['password'], $details['port'], $details['db'])) return false;
if(!self::Database()->isConnected()) return false;
self::Database()->setPrefix($details['tbl_prefix']);
self::Database()->setCharacterEncoding();
self::Database()->setCharacterSet();
// Configuration for Slave
$slave_details = Symphony::Configuration()->get('database_slave');
if($slave_details) {
if(!self::Database()->connect($slave_details['host'], $slave_details['user'], $slave_details['password'], $slave_details['port'], $slave_details['db'], 'slave')) return false;
if(!self::Database()->isConnected('slave')) return false;
self::Database()->setPrefix($details['tbl_prefix'], 'slave');
self::Database()->setCharacterEncoding('utf-8', 'slave');
self::Database()->setCharacterSet();
}
if(self::Configuration()->get('query_caching', 'database') == 'off') self::Database()->disableCaching();
elseif(self::Configuration()->get('query_caching', 'database') == 'on') self::Database()->enableCaching();
}
catch(DatabaseException $e){
$this->throwCustomError(
$e->getDatabaseErrorCode() . ': ' . $e->getDatabaseErrorMessage(),
__('Symphony Database Error'),
Page::HTTP_STATUS_ERROR,
'database',
array(
'error' => $e,
'message' => __('There was a problem whilst attempting to establish a database connection. Please check all connection information is correct.') . ' ' . __('The following error was returned:')
)
);
}
return true;
}
/**
* Attempts to log an Author in given a username and password.
* If the password is not hashed, it will be hashed using the sha1
* algorithm. The username and password will be sanitized before
* being used to query the Database. If an Author is found, they
* will be logged in and the sanitized username and password (also hashed)
* will be saved as values in the `$Cookie`.
*
* @see toolkit.General#hash()
* @param string $username
* The Author's username. This will be sanitized before use.
* @param string $password
* The Author's password. This will be sanitized and then hashed before use
* @param boolean $isHash
* If the password provided is already hashed, setting this parameter to
* true will stop it becoming rehashed. By default it is false.
* @return boolean
* True if the Author was logged in, false otherwise
*/
public function login($username, $password, $isHash=false){
$username = self::Database()->cleanValue($username);
$password = self::Database()->cleanValue($password);
if(strlen(trim($username)) > 0 && strlen(trim($password)) > 0){
$author = AuthorManager::fetch('id', 'ASC', 1, null, sprintf("
`username` = '%s'
", $username
));
if(!empty($author) && Cryptography::compare($password, current($author)->get('password'), $isHash)) {
$this->Author = current($author);
// Only migrate hashes if there is no update available as the update might change the tbl_authors table.
if($this->isUpgradeAvailable() === false && Cryptography::requiresMigration($this->Author->get('password'))){
$this->Author->set('password', Cryptography::hash($password));
self::Database()->update(array('password' => $this->Author->get('password')), 'tbl_authors', " `id` = '" . $this->Author->get('id') . "'");
}
$this->Cookie->set('username', $username);
$this->Cookie->set('pass', $this->Author->get('password'));
self::Database()->update(array(
'last_seen' => DateTimeObj::get('Y-m-d H:i:s')),
'tbl_authors',
sprintf(" `id` = %d", $this->Author->get('id'))
);
return true;
}
}
return false;
}
/**
* Symphony allows Authors to login via the use of tokens instead of
* a username and password. A token is derived from concatenating the
* Author's username and password and applying the sha1 hash to
* it, from this, a portion of the hash is used as the token. This is a useful
* feature often used when setting up other Authors accounts or if an
* Author forgets their password.
*
* @param string $token
* The Author token, which is a portion of the hashed string concatenation
* of the Author's username and password
* @return boolean
* True if the Author is logged in, false otherwise
*/
public function loginFromToken($token){
$token = self::Database()->cleanValue($token);
if(strlen(trim($token)) == 0) return false;
if(strlen($token) == 6){
$row = self::Database()->fetchRow(0, sprintf("
SELECT `a`.`id`, `a`.`username`, `a`.`password`
FROM `tbl_authors` AS `a`, `tbl_forgotpass` AS `f`
WHERE `a`.`id` = `f`.`author_id`
AND `f`.`expiry` > '%s'
AND `f`.`token` = '%s'
LIMIT 1
",
DateTimeObj::getGMT('c'), $token
));
self::Database()->delete('tbl_forgotpass', " `token` = '{$token}' ");
}
else{
$row = self::Database()->fetchRow(0, sprintf(
"SELECT `id`, `username`, `password`
FROM `tbl_authors`
WHERE SUBSTR(%s(CONCAT(`username`, `password`)), 1, 8) = '%s'
AND `auth_token_active` = 'yes'
LIMIT 1",
'SHA1', $token
));
}
if($row){
$this->Author = AuthorManager::fetchByID($row['id']);
$this->Cookie->set('username', $row['username']);
$this->Cookie->set('pass', $row['password']);
self::Database()->update(array('last_seen' => DateTimeObj::getGMT('Y-m-d H:i:s')), 'tbl_authors', " `id` = '$id'");
return true;
}
return false;
}
/**
* This function will destroy the currently logged in `$Author`
* session, essentially logging them out.
*
* @see core.Cookie#expire()
*/
public function logout(){
$this->Cookie->expire();
}
/**
* This function determines whether an there is a currently logged in
* Author for Symphony by using the `$Cookie`'s username
* and password. If an Author is found, they will be logged in, otherwise
* the `$Cookie` will be destroyed.
*
* @see core.Cookie#expire()
*/
public function isLoggedIn(){
// Ensures that we're in the real world.. Also reduces three queries from database
// We must return true otherwise exceptions are not shown
if (is_null(self::$_instance)) return true;
if ($this->Author){
return true;
}
else{
$username = self::Database()->cleanValue($this->Cookie->get('username'));
$password = self::Database()->cleanValue($this->Cookie->get('pass'));
if(strlen(trim($username)) > 0 && strlen(trim($password)) > 0){
$author = AuthorManager::fetch('id', 'ASC', 1, null, sprintf("
`username` = '%s'
", $username
));
if(!empty($author) && Cryptography::compare($password, current($author)->get('password'), true)) {
$this->Author = current($author);
self::Database()->update(array(
'last_seen' => DateTimeObj::get('Y-m-d H:i:s')),
'tbl_authors',
sprintf(" `id` = %d", $this->Author->get('id'))
);
// Only set custom author language in the backend
if(class_exists('Administration')) {
Lang::set($this->Author->get('language'));
}
return true;
}
}
$this->Cookie->expire();
return false;
}
}
/**
* Returns the most recent version found in the `/install/migrations` folder.
* Returns a version string to be used in `version_compare()` if an updater
* has been found. Returns `FALSE` otherwise.
*
* @since Symphony 2.3.1
* @return mixed
*/
public function getMigrationVersion(){
if($this->isInstallerAvailable()){
$migrations = scandir(DOCROOT . '/install/migrations');
$migration_file = end($migrations);
include_once(DOCROOT . '/install/lib/class.migration.php');
include_once(DOCROOT . '/install/migrations/' . $migration_file);
$migration_class = 'migration_' . str_replace('.', '', substr($migration_file, 0, -4));
return call_user_func(array($migration_class, 'getVersion'));
}
else{
return FALSE;
}
}
/**
* Checks if an update is available and applicable for the current installation.
*
* @since Symphony 2.3.1
* @return boolean
*/
public function isUpgradeAvailable(){
if($this->isInstallerAvailable()){
$migration_version = $this->getMigrationVersion();
$current_version = Symphony::Configuration()->get('version', 'symphony');
return version_compare($current_version, $migration_version, '<');
}
else{
return FALSE;
}
}
/**
* Checks if the installer/upgrader is available.
*
* @since Symphony 2.3.1
* @return boolean
*/
public function isInstallerAvailable(){
return file_exists(DOCROOT . '/install/index.php');
}
/**
* A wrapper for throwing a new Symphony Error page.
*
* @deprecated @since Symphony 2.3.2
*
* @see `throwCustomError`
* @param string $heading
* A heading for the error page
* @param string|XMLElement $message
* A description for this error, which can be provided as a string
* or as an XMLElement.
* @param string $template
* A string for the error page template to use, defaults to 'generic'. This
* can be the name of any template file in the `TEMPLATES` directory.
* A template using the naming convention of `tpl.*.php`.
* @param array $additional
* Allows custom information to be passed to the Symphony Error Page
* that the template may want to expose, such as custom Headers etc.
*/
public function customError($heading, $message, $template='generic', array $additional=array()){
$this->throwCustomError($message, $heading, Page::HTTP_STATUS_ERROR, $template, $additional);
}
/**
* A wrapper for throwing a new Symphony Error page.
*
* This methods sets the `GenericExceptionHandler::$enabled` value to `true`.
*
* @see core.SymphonyErrorPage
* @param string|XMLElement $message
* A description for this error, which can be provided as a string
* or as an XMLElement.
* @param string $heading
* A heading for the error page
* @param integer $status
* Properly sets the HTTP status code for the response. Defaults to
* `Page::HTTP_STATUS_ERROR`. Use `Page::HTTP_STATUS_XXX` to set this value.
* @param string $template
* A string for the error page template to use, defaults to 'generic'. This
* can be the name of any template file in the `TEMPLATES` directory.
* A template using the naming convention of `tpl.*.php`.
* @param array $additional
* Allows custom information to be passed to the Symphony Error Page
* that the template may want to expose, such as custom Headers etc.
*/
public function throwCustomError($message, $heading='Symphony Fatal Error', $status=Page::HTTP_STATUS_ERROR, $template='generic', array $additional=array()){
GenericExceptionHandler::$enabled = true;
throw new SymphonyErrorPage($message, $heading, $template, $additional, $status);
}
/**
* Setter accepts a previous Exception. Useful for determining the context
* of a current exception (ie. detecting recursion).
*
* @since Symphony 2.3.2
* @param Exception $ex
*/
public function setException(Exception $ex) {
$this->exception = $ex;
}
/**
* Accessor for `$this->exception`.
*
* @since Symphony 2.3.2
* @return Exception|null
*/
public function getException() {
return $this->exception;
}
/**
* Given the `$page_id` and a `$column`, this function will return an
* array of the given `$column` for the Page, including all parents.
*
* @deprecated This function will be removed in Symphony 2.4. Use
* `PageManager::resolvePage` instead.
* @param mixed $page_id
* The ID of the Page that currently being viewed, or the handle of the
* current Page
* @return array
* An array of the current Page, containing the `$column`
* requested. The current page will be the last item the array, as all
* parent pages are prepended to the start of the array
*/
public function resolvePage($page_id, $column) {
return PageManager::resolvePage($page_id, $column);
}
/**
* Given the `$page_id`, return the complete title of the
* current page.
*
* @deprecated This function will be removed in Symphony 2.4. Use
* `PageManager::resolvePageTitle` instead.
* @param mixed $page_id
* The ID of the Page that currently being viewed, or the handle of the
* current Page
* @return string
* The title of the current Page. If the page is a child of another
* it will be prepended by the parent and a colon, ie. Articles: Read
*/
public function resolvePageTitle($page_id) {
return PageManager::resolvePage($page_id, 'title');
}
/**
* Given the `$page_id`, return the complete path to the
* current page.
*
* @deprecated This function will be removed in Symphony 2.4. Use
* `PageManager::resolvePagePath` instead.
* @param mixed $page_id
* The ID of the Page that currently being viewed, or the handle of the
* current Page
* @return string
* The complete path to the current Page including any parent
* Pages, ie. /articles/read
*/
public function resolvePagePath($page_id) {
return PageManager::resolvePage($page_id, 'handle');
}
/**
* Returns the page namespace based on the current URL.
* A few examples:
*
* /login
* /publish
* /blueprints/datasources
* [...]
* /extension/$extension_name/$page_name
*
* This method is especially useful in couple with the translation function.
*
* @see toolkit#__()
* @return string
* The page namespace, without any action string (e.g. "new", "saved") or
* any value that depends upon the single setup (e.g. the section handle in
* /publish/$handle)
*/
public static function getPageNamespace() {
if(self::$namespace !== false) return self::$namespace;
$page = getCurrentPage();
if(!is_null($page)) $page = trim($page, '/');
if(substr($page, 0, 7) == 'publish') {
self::$namespace = '/publish';
}
else if(empty($page) && isset($_REQUEST['mode'])) {
self::$namespace = '/login';
}
else if(empty($page)) {
self::$namespace = null;
}
else {
$bits = explode('/', $page);
if($bits[0] == 'extension') {
self::$namespace = sprintf('/%s/%s/%s', $bits[0], $bits[1], $bits[2]);
}
else {
self::$namespace = sprintf('/%s/%s', $bits[0], $bits[1]);
}
}
return self::$namespace;
}
}
/**
* The `SymphonyErrorPageHandler` extends the `GenericExceptionHandler`
* to allow the template for the exception to be provided from the `TEMPLATES`
* directory
*/
Class SymphonyErrorPageHandler extends GenericExceptionHandler {
/**
* The render function will take a `SymphonyErrorPage` exception and
* output a HTML page. This function first checks to see if their is a custom
* template for this exception otherwise it reverts to using the default
* `usererror.generic.php`
*
* @param Exception $e
* The Exception object
* @return string
* An HTML string
*/
public static function render(Exception $e){
if($e->getTemplate() === false){
Page::renderStatusCode($e->getHttpStatusCode());
if(isset($e->getAdditional()->header)) header($e->getAdditional()->header);
echo '<h1>Symphony Fatal Error</h1><p>'.$e->getMessage().'</p>';
exit;
}
include($e->getTemplate());
}
}
/**
* `SymphonyErrorPage` extends the default `Exception` class. All
* of these exceptions will halt execution immediately and return the
* exception as a HTML page. By default the HTML template is `usererror.generic.php`
* from the `TEMPLATES` directory.
*/
Class SymphonyErrorPage extends Exception{
/**
* A heading for the error page, this will be prepended to
* "Symphony Fatal Error".
* @return string
*/
private $_heading;
/**
* A string for the error page template to use, defaults to 'generic'. This
* can be the name of any template file in the `TEMPLATES` directory.
* A template using the naming convention of `usererror.*.php`.
* @var string
*/
private $_template = 'generic';
/**
* If the message as provided as an `XMLElement`, it will be saved to
* this parameter
* @var XMLElement
*/
private $_messageObject = null;
/**
* An object of an additional information for this error page. Note that
* this is provided as an array and then typecast to an object
* @var StdClass
*/
private $_additional = null;
/**
* A simple container for the response status code.
* Full value is setted usign `$Page->setHttpStatus()`
* in the template.
*/
private $_status = Page::HTTP_STATUS_ERROR;
/**
* Constructor for SymphonyErrorPage sets it's class variables
*
* @param string|XMLElement $message
* A description for this error, which can be provided as a string
* or as an XMLElement.
* @param string $heading
* A heading for the error page, by default this is "Symphony Fatal Error"
* @param string $template
* A string for the error page template to use, defaults to 'generic'. This
* can be the name of any template file in the `TEMPLATES` directory.
* A template using the naming convention of `tpl.*.php`.
* @param array $additional
* Allows custom information to be passed to the Symphony Error Page
* that the template may want to expose, such as custom Headers etc.
* @param integer $status
* Properly sets the HTTP status code for the response. Defaults to
* `Page::HTTP_STATUS_ERROR`
*/
public function __construct($message, $heading='Symphony Fatal Error', $template='generic', array $additional=array(), $status=Page::HTTP_STATUS_ERROR){
if($message instanceof XMLElement){
$this->_messageObject = $message;
$message = $this->_messageObject->generate();
}
parent::__construct($message);
$this->_heading = $heading;
$this->_template = $template;
$this->_additional = (object)$additional;
$this->_status = $status;
}
/**
* Accessor for the `$_heading` of the error page
*
* @return string
*/
public function getHeading(){
return $this->_heading;
}
/**
* Accessor for `$_messageObject`
*
* @return XMLElement
*/
public function getMessageObject(){
return $this->_messageObject;
}
/**
* Accessor for `$_additional`
*
* @return StdClass
*/
public function getAdditional(){
return $this->_additional;
}
/**
* Accessor for `$_status`
*
* @since Symphony 2.3.2
* @return integer
*/
public function getHttpStatusCode() {
return $this->_status;
}
/**
* Returns the path to the current template by looking at the
* `WORKSPACE/template/` directory, then at the `TEMPLATES`
* directory for the convention `usererror.*.php`. If the template
* is not found, `false` is returned
*
* @since Symphony 2.3
* @return mixed
* String, which is the path to the template if the template is found,
* false otherwise
*/
public function getTemplate(){
$format = '%s/usererror.%s.php';
if(file_exists($template = sprintf($format, WORKSPACE . '/template', $this->_template)))
return $template;
elseif(file_exists($template = sprintf($format, TEMPLATE, $this->_template)))
return $template;
else
return false;
}
/**
* A simple getter to the template name in order to be able
* to identify which type of exception this is.
*
* @since Symphony 2.3.2
* @return string
*/
public function getTemplateName() {
return $this->_template;
}
}
/**
* The `DatabaseExceptionHandler` provides a render function to provide
* customised output for database exceptions. It displays the exception
* message as provided by the Database.
*/
Class DatabaseExceptionHandler extends GenericExceptionHandler {
/**
* The render function will take a `DatabaseException` and output a
* HTML page.
*
* @param Exception $e
* The Exception object
* @return string
* An HTML string
*/
public static function render(Exception $e) {
$trace = $queries = null;
foreach($e->getTrace() as $t){
$trace .= sprintf(
'<li><code><em>[%s:%d]</em></code></li><li><code>&#160;&#160;&#160;&#160;%s%s%s();</code></li>',
$t['file'],
$t['line'],
(isset($t['class']) ? $t['class'] : null),
(isset($t['type']) ? $t['type'] : null),
$t['function']
);
}
if(is_object(Symphony::Database())){
$debug = Symphony::Database()->debug();
if(!empty($debug)) foreach($debug as $query){
$queries .= sprintf(
'<li><em>[%01.4f]</em><code> %s;</code> </li>',
(isset($query['execution_time']) ? $query['execution_time'] : null),
htmlspecialchars($query['query'])
);
}
}
$html = sprintf(file_get_contents(self::getTemplate('fatalerror.database')),
$e->getDatabaseErrorMessage(),
$e->getQuery(),
$trace,
$queries
);
return str_replace('{SYMPHONY_URL}', SYMPHONY_URL, $html);
}
}
@mathijsmaliepaard
Copy link

Thanks! Exactly what I was looking for! Not only the mysqli but also the master/slave, just great!

When using mysqli for a fresh install you run into the problem that the installer still references mysql (checks on function mysql_connect). For cloning it would be great if an updated installer class is included in this gist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment