mirror of
				https://github.com/beestat/app.git
				synced 2025-10-31 10:07:01 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			884 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			884 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| namespace cora;
 | |
| 
 | |
| /**
 | |
|  * This exception is thrown by database->query if the query failed due to a
 | |
|  * duplicate entry in the database.
 | |
|  *
 | |
|  * @author Jon Ziebell
 | |
|  */
 | |
| final class DuplicateEntryException extends \Exception {
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * This is a MySQLi database wrapper. It provides access to some basic
 | |
|  * functions like select, insert, and update. Those functions automatically
 | |
|  * escape table names, column names, and parameters for you using a number of
 | |
|  * the private functions defined here.
 | |
|  *
 | |
|  * Alternatively, you can write your own queries (and use the escape()
 | |
|  * function to help), and just call query() to run your own.
 | |
|  *
 | |
|  * @author Jon Ziebell
 | |
|  */
 | |
| final class database extends \mysqli {
 | |
| 
 | |
|   /**
 | |
|    * The singleton.
 | |
|    *
 | |
|    * @var database
 | |
|    */
 | |
|   private static $instance;
 | |
| 
 | |
|   /**
 | |
|    * A database singleton that does not use transactions.
 | |
|    *
 | |
|    * @var database
 | |
|    */
 | |
|   private static $transactionless_instance;
 | |
| 
 | |
|   /**
 | |
|    * Column types
 | |
|    *
 | |
|    * @var array
 | |
|    */
 | |
|   private static $types;
 | |
| 
 | |
|   /**
 | |
|    * Whether or not to use transactions in this connection.
 | |
|    *
 | |
|    * @var bool
 | |
|    */
 | |
|   private $use_transactions = true;
 | |
| 
 | |
|   /**
 | |
|    * Whether or not a transaction has been started. Used to make sure only one
 | |
|    * is started at a time and it gets closed only if it's open.
 | |
|    *
 | |
|    * @var bool
 | |
|    */
 | |
|   private $transaction_started = false;
 | |
| 
 | |
|   /**
 | |
|    * The total number of queries executed.
 | |
|    *
 | |
|    * @var int
 | |
|    */
 | |
|   private $query_count = 0;
 | |
| 
 | |
|   /**
 | |
|    * The total time all queries have taken to execute.
 | |
|    *
 | |
|    * @var float
 | |
|    */
 | |
|   private $query_time = 0;
 | |
| 
 | |
|   /**
 | |
|    * The cora object.
 | |
|    *
 | |
|    * @var cora
 | |
|    */
 | |
|   private $cora;
 | |
| 
 | |
|   /**
 | |
|    * The setting object.
 | |
|    *
 | |
|    * @var setting
 | |
|    */
 | |
|   private $setting;
 | |
| 
 | |
|   /**
 | |
|    * Create the mysql object used for the current API call and start a
 | |
|    * transaction. The same transaction is used for all queries on this
 | |
|    * connection, even in the case of a multi-api call. The transaction is
 | |
|    * auto- closed upon destruction of this class.
 | |
|    *
 | |
|    * This function is private because this class is a singleton and should be
 | |
|    * instantiated using the get_instance() function.
 | |
|    *
 | |
|    * @throws \Exception If failing to connect to the database.
 | |
|    */
 | |
|   public function __construct() {
 | |
|     $this->cora = cora::get_instance();
 | |
|     $this->setting = setting::get_instance();
 | |
| 
 | |
|     parent::__construct(
 | |
|       $this->setting->get('database_host'),
 | |
|       $this->setting->get('database_username'),
 | |
|       $this->setting->get('database_password')
 | |
|     );
 | |
| 
 | |
|     // Have the database driver return ints and floats in PHP native types. PHP
 | |
|     // does not have a native type for decimals so that gets left behind.
 | |
|     parent::options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
 | |
| 
 | |
|     if($this->connect_error !== null) {
 | |
|       $this->cora->set_error_extra_info(
 | |
|         [
 | |
|           'database_error' => $this->connect_error
 | |
|         ]
 | |
|       );
 | |
|       throw new \Exception('Could not connect to database.', 1200);
 | |
|     }
 | |
| 
 | |
|     $database_name = $this->setting->get('database_name');
 | |
|     if($database_name !== null) {
 | |
|       $success = $this->select_db($database_name);
 | |
|       if($success === false) {
 | |
| 
 | |
|         $this->cora->set_error_extra_info(
 | |
|           [
 | |
|             'database_error' => $this->error
 | |
|           ]
 | |
|         );
 | |
|         throw new \Exception('Could not select database.', 1208);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Upon destruction of this class, close the open transaction. I check to
 | |
|    * make sure one is open, but that should really always be the case since
 | |
|    * one gets opened regardless.
 | |
|    */
 | |
|   public function __destruct() {
 | |
|     if($this->transaction_started === true) {
 | |
|       $this->commit_transaction();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Use this function to instantiate this class instead of calling new
 | |
|    * database() (which isn't allowed anyways). This avoids confusion from
 | |
|    * trying to use dependency injection by passing an instance of this class
 | |
|    * around everywhere. It also keeps a single connection open to the database
 | |
|    * for the current API call.
 | |
|    *
 | |
|    * @return database A new database object or the already created one.
 | |
|    */
 | |
|   public static function get_instance() {
 | |
|     if(isset(self::$instance) === false) {
 | |
|       self::$instance = new self();
 | |
|     }
 | |
| 
 | |
|     return self::$instance;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Second instance; see comment on the class member declaration.
 | |
|    *
 | |
|    * @return database A new database object or the already created one.
 | |
|    */
 | |
|   public static function get_transactionless_instance() {
 | |
|     if(isset(self::$transactionless_instance) === false) {
 | |
|       self::$transactionless_instance = new self();
 | |
|       self::$transactionless_instance->disable_transactions();
 | |
|     }
 | |
| 
 | |
|     return self::$transactionless_instance;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Start a transaction.
 | |
|    *
 | |
|    * @throws \Exception If the transaction fails to start.
 | |
|    */
 | |
|   public function start_transaction() {
 | |
|     if($this->transaction_started === false) {
 | |
|       $result = $this->query('start transaction');
 | |
|       if($result === false) {
 | |
|         throw new \Exception('Failed to start database transaction.', 1201);
 | |
|       }
 | |
|       $this->transaction_started = true;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Commit a transaction.
 | |
|    *
 | |
|    * @throws \Exception If the transaction fails to commit.
 | |
|    */
 | |
|   public function commit_transaction() {
 | |
|     if($this->transaction_started === true) {
 | |
|       $this->transaction_started = false;
 | |
|       $result = $this->query('commit');
 | |
|       if($result === false) {
 | |
|         throw new \Exception('Failed to commit database transaction.', 1202);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Rollback the current transaction. This is exposed because the exception
 | |
|    * handler needs to rollback the current transaction when it runs.
 | |
|    *
 | |
|    * @throws \Exception If the transaction fails to rollback.
 | |
|    */
 | |
|   public function rollback_transaction() {
 | |
|     if($this->transaction_started === true) {
 | |
|       $this->transaction_started = false;
 | |
|       $result = $this->query('rollback');
 | |
|       if($result === false) {
 | |
|         throw new \Exception('Failed to rollback database transaction.', 1203);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Escape a value to be used in a query. Only necessary when doing custom
 | |
|    * queries. All helper functions like select, insert, and update escape
 | |
|    * values for you using this function.
 | |
|    *
 | |
|    * @param mixed $value The value to escape. Boolean true and false are
 | |
|    * converted to int 1 and 0 respectively.
 | |
|    * @param bool $basic If overridden to true, just return real_escape_string
 | |
|    * of $value. If left alone or set to false, return a value appropriate to
 | |
|    * be used like "set foo=$bar" as it will have single quotes around it if
 | |
|    * necessary.
 | |
|    *
 | |
|    * @return string The escaped value.
 | |
|    */
 | |
|   public function escape($value, $basic = false) {
 | |
|     if($basic === true) {
 | |
|       return $this->real_escape_string($value);
 | |
|     }
 | |
| 
 | |
|     if($value === null) {
 | |
|       return 'null';
 | |
|     }
 | |
|     else if($value === true) {
 | |
|       return '1';
 | |
|     }
 | |
|     else if($value === false) {
 | |
|       return '0';
 | |
|     }
 | |
|     else if(is_int($value) === true) {
 | |
|       return $value;
 | |
|     }
 | |
|     else {
 | |
|       return '"' . $this->real_escape_string($value) . '"';
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Helper function to secure names of tables & columns passed to this class.
 | |
|    * First of all, these identifiers must be a valid word. Backticks are also
 | |
|    * placed around the identifier in all cases to allow the use of MySQL
 | |
|    * keywords as table and column names.
 | |
|    *
 | |
|    * @param string $identifier The identifier to escape
 | |
|    *
 | |
|    * @throws \Exception If the identifier does not match the character class
 | |
|    * [A-Za-z0-9_]. That would make it invalid for use in MySQL.
 | |
|    *
 | |
|    * @return string The escaped identifier.
 | |
|    */
 | |
|   public function escape_identifier($identifier) {
 | |
|     if(preg_match('/^\w+$/', $identifier) === 1) {
 | |
|       return '`' . $identifier . '`';
 | |
|     }
 | |
|     else {
 | |
|       $this->cora->set_error_extra_info(
 | |
|         [
 | |
|           'identifier' => $identifier
 | |
|         ]
 | |
|       );
 | |
|       throw new \Exception('Query identifier is invalid.', 1204);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Builds a properly escaped string for the 'where column=value' portion of
 | |
|    * a query.
 | |
|    *
 | |
|    * @param string $column The query column.
 | |
|    * @param mixed $value The value(s) to compare against. You can use null, an
 | |
|    * array, or any other value here and the appropriate comparison (is null,
 | |
|    * in, =) will be used.
 | |
|    *
 | |
|    * @throws \Exception If an invalid operator was specified.
 | |
|    *
 | |
|    * @return string The appropriate escaped string. Examples: `foo` is null
 | |
|    * `foo` in(1,2,3) `foo`='bar'
 | |
|    */
 | |
|   private function column_equals_value_where($column, $value) {
 | |
|     if($value === null) {
 | |
|       return $this->escape_identifier($column) . ' is null';
 | |
|     }
 | |
|     else if(is_array($value) === true) {
 | |
|       if(isset($value['operator']) === true) {
 | |
|         if(in_array($value['operator'], ['>', '<', '=', '>=', '<=', 'between']) === false) {
 | |
|           throw new \Exception('Invalid operator', 1213);
 | |
|         }
 | |
|         if($value['operator'] === 'between') {
 | |
|           return $this->escape_identifier($column) . ' between ' . $this->escape($value['value'][0]) . ' and ' . $this->escape($value['value'][1]);
 | |
|         } else {
 | |
|           return $this->escape_identifier($column) . $value['operator'] . $this->escape($value['value']);
 | |
|         }
 | |
|       }
 | |
|       else {
 | |
|         return $this->escape_identifier($column) .
 | |
|           ' in (' . implode(',', array_map([$this, 'escape'], $value)) . ')';
 | |
|       }
 | |
|     }
 | |
|     else {
 | |
|       return $this->escape_identifier($column) . '=' . $this->escape($value);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Builds a properly escaped string for the 'set column=value' portion of a
 | |
|    * query.
 | |
|    *
 | |
|    * @param string $column The query column.
 | |
|    * @param mixed $value The value to set.
 | |
|    *
 | |
|    * @return string The appropriate escaped string. Examples: `foo`='bar'
 | |
|    * `foo`=5
 | |
|    */
 | |
|   private function column_equals_value_set($column, $value) {
 | |
|     return $this->escape_identifier($column) . '=' . $this->escape($value);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Performs a query on the database. This function is available publicly for
 | |
|    * the case when the standard select, insert, and update don't quite cut it.
 | |
|    *
 | |
|    * The exceptions are broken up somewhat by type to make it easier to catch
 | |
|    * and handle these exceptions if desired.
 | |
|    *
 | |
|    * This will start a transaction if the query begins with 'insert',
 | |
|    * 'update', or 'delete' and a transaction has not already been started.
 | |
|    *
 | |
|    * IMPORTANT: YOU MUST SANTIZE YOUR OWN DATABASE QUERY WHEN USING THIS
 | |
|    * FUNCTION DIRECTLY. THIS FUNCTION DOES NOT DO IT FOR YOU.
 | |
|    *
 | |
|    * @param string $query The query to execute.
 | |
|    * @param int $resultmode Just here because PHP requires it.
 | |
|    *
 | |
|    * @throws DuplicateEntryException if the query failed due to a duplicate
 | |
|    * entry (unique key violation)
 | |
|    * @throws \Exception If the query failed and was not caught by any other
 | |
|    * exception types.
 | |
|    *
 | |
|    * @return mixed The result directly from $mysqli->query.
 | |
|    */
 | |
|   public function query($query, $resultmode = null) {
 | |
|     // If this was an insert, update or delete, start a transaction
 | |
|     $query_type = substr(trim($query), 0, 6);
 | |
|     if(
 | |
|       in_array($query_type, ['insert', 'update', 'delete']) === true &&
 | |
|       $this->use_transactions === true
 | |
|     ) {
 | |
|       $this->start_transaction();
 | |
|     }
 | |
| 
 | |
|     $start = microtime(true);
 | |
|     $result = parent::query($query);
 | |
|     $stop = microtime(true);
 | |
| 
 | |
|     if($result === false) {
 | |
|       $database_error = $this->error;
 | |
|       $this->rollback_transaction();
 | |
| 
 | |
|       $this->cora->set_error_extra_info(
 | |
|         [
 | |
|           'database_error' => $database_error,
 | |
|           'query' => $query
 | |
|         ]
 | |
|       );
 | |
| 
 | |
|       if(stripos($database_error, 'duplicate entry') !== false) {
 | |
|         throw new DuplicateEntryException('Duplicate database entry.', 1205);
 | |
|       }
 | |
|       else {
 | |
|         throw new \Exception('Database query failed.', 1206);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Don't log info about transactions...they're a wash
 | |
|     $this->query_count++;
 | |
|     $this->query_time += ($stop - $start);
 | |
| 
 | |
|     return $result;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Select some columns from some table with some where clause.
 | |
|    *
 | |
|    * @param string $resource The resource to select from.
 | |
|    * @param array $attributes An array of key value pairs to search by and can
 | |
|    * include arrays if you want to search in() something.
 | |
|    * @param array $columns The columns to return. If not specified, all
 | |
|    * columns are returned.
 | |
|    * @param mixed $order_by String or array of order_bys.
 | |
|    *
 | |
|    * @return array An array of the database rows with the specified columns.
 | |
|    * Even a single result will still be returned in an array of size 1.
 | |
|    */
 | |
|   public function read($resource, $attributes = [], $columns = [], $order_by = []) {
 | |
|     $table = $this->get_table($resource);
 | |
| 
 | |
|     // Build the column listing.
 | |
|     if(count($columns) === 0) {
 | |
|       $columns = '*';
 | |
|     }
 | |
|     else {
 | |
|       $columns = implode(
 | |
|         ',',
 | |
|         array_map([$this, 'escape_identifier'], $columns)
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     // Remove empty arrays from the attributes to avoid query errors on the
 | |
|     // empty in() statement.
 | |
|     foreach($attributes as $key => $value) {
 | |
|       if(is_array($value) === true && count($value) === 0) {
 | |
|         unset($attributes[$key]);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Build the where clause.
 | |
|     if (count($attributes) === 0) {
 | |
|       $where = '';
 | |
|     } else {
 | |
|       $where = ' where ' .
 | |
|         implode(
 | |
|           ' and ',
 | |
|           array_map(
 | |
|             [$this, 'column_equals_value_where'],
 | |
|             array_keys($attributes),
 | |
|             $attributes
 | |
|           )
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     if (is_array($order_by) === false) {
 | |
|       $order_by = [$order_by];
 | |
|     }
 | |
| 
 | |
|     if (count($order_by) === 0) {
 | |
|       $order_by = '';
 | |
|     } else {
 | |
|       $order_by = ' order by ' .
 | |
|         implode(
 | |
|           ',',
 | |
|           array_map(
 | |
|             [$this, 'escape_identifier'],
 | |
|             $order_by
 | |
|           )
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     // Put everything together and return the result.
 | |
|     $query = 'select ' . $columns . ' from ' .
 | |
|       $this->escape_identifier($table) . $where . $order_by;
 | |
|     $result = $this->query($query);
 | |
| 
 | |
|     /**
 | |
|      * Get a list of all fields that need to be cast to other data types. The
 | |
|      * MYSQLI_OPT_INT_AND_FLOAT_NATIVE flag handles ints and floats. This
 | |
|      * turns decimal into float and tinyint(1) into boolean.
 | |
|      */
 | |
|     $float_fields = [];
 | |
|     $boolean_fields = [];
 | |
|     $json_fields = [];
 | |
|     while($field_info = $result->fetch_field()) {
 | |
|       if($field_info->type === 1 && $field_info->length === 1) {
 | |
|         $boolean_fields[] = $field_info->name;
 | |
|       }
 | |
|       else if($field_info->type === 246) {
 | |
|         $float_fields[] = $field_info->name;
 | |
|       }
 | |
|       else if($field_info->type === 245) {
 | |
|         $json_fields[] = $field_info->name;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     $results = [];
 | |
|     while($row = $result->fetch_assoc()) {
 | |
|       // Cast if necessary.
 | |
|       foreach($float_fields as $float_field) {
 | |
|         $row[$float_field] = (float) $row[$float_field];
 | |
|       }
 | |
|       foreach($boolean_fields as $boolean_field) {
 | |
|         $row[$boolean_field] = (boolean) $row[$boolean_field];
 | |
|       }
 | |
|       foreach($json_fields as $json_field) {
 | |
|         $row[$json_field] = json_decode($row[$json_field], true);
 | |
|       }
 | |
| 
 | |
| 
 | |
|       // Diverge the converged column.
 | |
|       if(
 | |
|         isset($row['converged']) === true &&
 | |
|         class_exists($resource) === true && // This will also call the autoloader to make sure it's loaded
 | |
|         isset($resource::$converged) === true
 | |
|       ) {
 | |
|         foreach($resource::$converged as $column => $column_properties) {
 | |
|           if(isset($row[$column]) === true) {
 | |
|             throw new \Exception('Column `' . $column . '` exists; cannot be overwritten by converged column.', 1212);
 | |
|           }
 | |
|           $row[$column] = (isset($row['converged'][$column]) === true) ? $row['converged'][$column] : null;
 | |
|         }
 | |
|         unset($row['converged']);
 | |
|       }
 | |
| 
 | |
|       $results[] = $row;
 | |
|     }
 | |
| 
 | |
|     return $results;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Converge attributes.
 | |
|    *
 | |
|    * @param string $resource
 | |
|    * @param int $id
 | |
|    * @param array $attributes
 | |
|    *
 | |
|    * @return array
 | |
|    */
 | |
|   private function converge_attributes($resource, $id, $attributes) {
 | |
|     if(
 | |
|       class_exists($resource) === true && // This will also call the autoloader to make sure it's loaded
 | |
|       isset($resource::$converged) === true &&
 | |
|       empty($resource::$converged) === false
 | |
|     ) {
 | |
|       $table = $this->get_table($resource);
 | |
| 
 | |
|       if($id !== null) {
 | |
|         $existing_attributes = [];
 | |
|         $existing_attributes[$table . '_id'] = $id;
 | |
|         $existing_row = $this->read($resource, $existing_attributes)[0];
 | |
|       }
 | |
|       else {
 | |
|         $existing_row = [];
 | |
|       }
 | |
| 
 | |
|       $converged = [];
 | |
|       foreach($resource::$converged as $column => $column_properties) {
 | |
|         if(isset($existing_row[$column]) === true) {
 | |
|           $converged[$column] = $existing_row[$column];
 | |
|         }
 | |
|         if(array_key_exists($column, $attributes) === true) {
 | |
|           if($attributes[$column] === null) {
 | |
|             unset($converged[$column]);
 | |
|           }
 | |
|           else {
 | |
|             switch($column_properties['type']) {
 | |
|               case 'int':
 | |
|               case 'float':
 | |
|               case 'string':
 | |
|                 settype($attributes[$column], $column_properties['type']);
 | |
|               break;
 | |
|             }
 | |
|             $converged[$column] = $attributes[$column];
 | |
|           }
 | |
|           unset($attributes[$column]);
 | |
|         }
 | |
|       }
 | |
|       $attributes['converged'] = json_encode($converged);
 | |
|     }
 | |
| 
 | |
|     return $attributes;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Update some columns in a table by the primary key. Doing updates without
 | |
|    * using the primary key are supported by writing your own queries and using
 | |
|    * the database->query() function. That should be a rare circumstance
 | |
|    * though.
 | |
|    *
 | |
|    * @param string $resource The resource to update.
 | |
|    * @param array $attributes The attributes to set.
 | |
|    * @param array $return_mode Either "row" or "id". Specifying row will
 | |
|    * return the newly created row (does a database read). Specifying id will
 | |
|    * return just the ID of the created row instead of performing another query
 | |
|    * to get the whole inserted row.
 | |
|    *
 | |
|    * @throws \Exception If no attributes were specified.
 | |
|    *
 | |
|    * @return int The updated row.
 | |
|    */
 | |
|   public function update($resource, $attributes, $return_mode = 'row') {
 | |
|     $table = $this->get_table($resource);
 | |
| 
 | |
|     foreach($attributes as $key => $value) {
 | |
|       if($this->get_type($resource, $key) === 'json') {
 | |
|         if($value === null) {
 | |
|           $attributes[$key] = null;
 | |
|         }
 | |
|         else {
 | |
|           $attributes[$key] = json_encode($value);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Check for errors
 | |
|     if(isset($attributes[$table . '_id']) === false) {
 | |
|       throw new \Exception('ID is required for update.', 1214);
 | |
|     }
 | |
| 
 | |
|     // Extract the ID.
 | |
|     $id = $attributes[$table . '_id'];
 | |
|     unset($attributes[$table . '_id']);
 | |
| 
 | |
|     // Check for errors
 | |
|     if(count($attributes) === 0) {
 | |
|       throw new \Exception('Updates require at least one attribute.', 1207);
 | |
|     }
 | |
| 
 | |
|     // Converge the diverged attributes.
 | |
|     $attributes = $this->converge_attributes($resource, $id, $attributes);
 | |
| 
 | |
|     // Build the column setting
 | |
|     $columns = implode(
 | |
|       ',',
 | |
|       array_map(
 | |
|         [$this, 'column_equals_value_set'],
 | |
|         array_keys($attributes),
 | |
|         $attributes
 | |
|       )
 | |
|     );
 | |
| 
 | |
|     // Build the where clause
 | |
|     $where_attributes = [$table . '_id' => $id];
 | |
|     $where = 'where ' .
 | |
|       implode(
 | |
|         ' and ',
 | |
|         array_map(
 | |
|           [$this, 'column_equals_value_where'],
 | |
|           array_keys($where_attributes),
 | |
|           $where_attributes
 | |
|         )
 | |
|       );
 | |
| 
 | |
|     $query = 'update ' . $this->escape_identifier($table) .
 | |
|       ' set ' . $columns . ' ' . $where;
 | |
| 
 | |
|     // Disallow updates in the demo.
 | |
|     if($this->setting->is_demo() === true) {
 | |
|       return $this->read($resource, $where_attributes)[0];
 | |
|     }
 | |
| 
 | |
|     $this->query($query);
 | |
| 
 | |
|     if($return_mode === 'row') {
 | |
|       return $this->read($resource, $where_attributes)[0];
 | |
|     } else {
 | |
|       return $id;
 | |
|     }
 | |
| 
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Actually delete a row from a table by the primary key.
 | |
|    *
 | |
|    * @param string $table The table to delete from.
 | |
|    * @param int $id The value of the primary key to delete.
 | |
|    *
 | |
|    * @return int The number of rows affected by the delete (could be 0).
 | |
|    */
 | |
|   public function delete($table, $id) {
 | |
|     $query = 'delete from ' . $this->escape_identifier($table) .
 | |
|       ' where ' . $this->escape_identifier($table . '_id') . ' = ' .
 | |
|       $this->escape($id);
 | |
|     $this->query($query);
 | |
| 
 | |
|     return $this->affected_rows;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Insert a row into the specified table. This only supports single-row
 | |
|    * inserts.
 | |
|    *
 | |
|    * @param string $table The table to insert into.
 | |
|    * @param array $attributes The attributes to set on the row.
 | |
|    * @param array $return_mode Either "row" or "id". Specifying row will
 | |
|    * return the newly created row (does a database read). Specifying id will
 | |
|    * return just the ID of the created row.
 | |
|    *
 | |
|    * @return int The primary key of the inserted row.
 | |
|    */
 | |
|   public function create($resource, $attributes, $return_mode = 'row') {
 | |
|     $table = $this->get_table($resource);
 | |
| 
 | |
|     foreach($attributes as $key => $value) {
 | |
|       if($this->get_type($resource, $key) === 'json') {
 | |
|         if($value === null) {
 | |
|           $attributes[$key] = null;
 | |
|         }
 | |
|         else {
 | |
|           $attributes[$key] = json_encode($value);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     $attributes = $this->converge_attributes($resource, null, $attributes);
 | |
| 
 | |
|     $columns = implode(
 | |
|       ',',
 | |
|       array_map([$this, 'escape_identifier'], array_keys($attributes))
 | |
|     );
 | |
| 
 | |
|     $values = implode(
 | |
|       ',',
 | |
|       array_map([$this, 'escape'], $attributes)
 | |
|     );
 | |
| 
 | |
|     $query =
 | |
|       'insert into ' . $this->escape_identifier($table) .
 | |
|       '(' . $columns . ') values (' . $values . ')';
 | |
| 
 | |
|     $this->query($query);
 | |
| 
 | |
|     if($return_mode === 'row') {
 | |
|       $read_attributes = [];
 | |
|       $read_attributes[$table . '_id'] = $this->insert_id;
 | |
|       return $this->read($resource, $read_attributes)[0];
 | |
|     } else {
 | |
|       return $this->insert_id;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Gets the number of queries that have been executed.
 | |
|    *
 | |
|    * @return int The query count.
 | |
|    */
 | |
|   public function get_query_count() {
 | |
|     return $this->query_count;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Gets the time taken to execute all of the queries.
 | |
|    *
 | |
|    * @return float The total execution time.
 | |
|    */
 | |
|   public function get_query_time() {
 | |
|     return $this->query_time;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Turn a resource into a table.
 | |
|    *
 | |
|    * @param string $resource
 | |
|    *
 | |
|    * @return string
 | |
|    */
 | |
|   private function get_table($resource) {
 | |
|     $class_parts = explode('\\', $resource);
 | |
|     return end($class_parts);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get the type of a specific column.
 | |
|    *
 | |
|    * @param string $table The table.
 | |
|    * @param string $column The column.
 | |
|    *
 | |
|    * @return string The type.
 | |
|    */
 | |
|   private function get_type($resource, $column) {
 | |
|     $table = $this->get_table($resource);
 | |
| 
 | |
|     // If this column is in converged, get the type from there.
 | |
|     if(
 | |
|       class_exists($resource) === true && // This will also call the autoloader to make sure it's loaded
 | |
|       isset($resource::$converged) === true &&
 | |
|       isset($resource::$converged[$column]) === true
 | |
|     ) {
 | |
|       return $resource::$converged[$column]['type'];
 | |
|     }
 | |
| 
 | |
|     // Otherwise query the entire schema (and cache it) to see what the type is.
 | |
|     if(isset(self::$types) === false) {
 | |
|       self::$types = [];
 | |
|       $result = $this->query('
 | |
|         select
 | |
|           `TABLE_NAME`,
 | |
|           `COLUMN_NAME`,
 | |
|           `DATA_TYPE`
 | |
|         from
 | |
|           `information_schema`.`columns`
 | |
|         where
 | |
|           `table_schema` = ' . $this->escape($this->setting->get('database_name')) . '
 | |
|       ');
 | |
|       while($row = $result->fetch_assoc()) {
 | |
|         self::$types[$row['TABLE_NAME'] . '.' . $row['COLUMN_NAME']] = $row['DATA_TYPE'];
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return self::$types[$table . '.' . $column];
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Attempt to get a database lock.
 | |
|    *
 | |
|    * @param string $lock_name The lock name.
 | |
|    * @param int $timeout How long to wait for the lock. Negative values are
 | |
|    * forever.
 | |
|    *
 | |
|    * @throws \Exception If the lock could not be obtained.
 | |
|    */
 | |
|   public function get_lock($lock_name, $timeout = 0) {
 | |
|     $result = $this->query('
 | |
|       select
 | |
|         get_lock(' .
 | |
|           $this->escape($this->setting->get('database_name') . '_' . $lock_name) . ', ' .
 | |
|           $this->escape($timeout) .
 | |
|         ') `lock`
 | |
|       ');
 | |
|     $row = $result->fetch_assoc();
 | |
|     if($row['lock'] !== 1) {
 | |
|       throw new \Exception('Could not get lock.', 1209);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Attempt to release a database lock.
 | |
|    *
 | |
|    * @param string $lock_name The lock name.
 | |
|    *
 | |
|    * @throws \Exception If the lock was not established by this thread.
 | |
|    * @throws \Exception If the lock does not exist.
 | |
|    */
 | |
|   public function release_lock($lock_name) {
 | |
|     $result = $this->query('
 | |
|       select
 | |
|         release_lock(' . $this->escape($this->setting->get('database_name') . '_' . $lock_name) . ') `lock`
 | |
|       ');
 | |
|     $row = $result->fetch_assoc();
 | |
|     if($row['lock'] === 0) {
 | |
|       throw new \Exception('Lock not established by this thread.', 1210);
 | |
|     }
 | |
|     else if($row['lock'] === null) {
 | |
|       throw new \Exception('Lock does not exist.', 1211);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Set the time zone. Useful for querying in a certain time zone so MySQL
 | |
|    * will just handle the conversion.
 | |
|    *
 | |
|    * @param int $time_zone_offset Offset in minutes.
 | |
|    */
 | |
|   public function set_time_zone($time_zone_offset) {
 | |
|     $operator = $time_zone_offset < 0 ? '-' : '+';
 | |
|     $time_zone_offset = abs($time_zone_offset);
 | |
|     $offset_hours = floor($time_zone_offset / 60);
 | |
|     $offset_minutes = $time_zone_offset % 60;
 | |
|     $this->query('SET time_zone = "' . $operator . sprintf('%d', $offset_hours) . ':' . str_pad($offset_minutes, 2, STR_PAD_LEFT) . '"');
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Disable transactions for this connection.
 | |
|    */
 | |
|   public function disable_transactions() {
 | |
|     $this->use_transactions = false;
 | |
|   }
 | |
| 
 | |
| }
 |