芝麻web文件管理V1.00
编辑当前文件:/home/ephorei/www/wp-content/plugins/sureforms/inc/database/base.php
*/ namespace SRFM\Inc\Database; use SRFM\Inc\Helper; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; /** * SureForms Database Tables Base Class * * @since 0.0.10 */ abstract class Base { /** * WordPress Database class instance. * * @var \wpdb * @since 0.0.10 */ protected $wpdb; /** * Current database table prefix mixed with 'srfm_' as ending. * * @var string * @since 0.0.10 */ protected $table_prefix; /** * Custom table suffix without any prefix. This needs to be overridden from child class. * Eg: For entries table, suffix will be 'entries' which will be prefixed and finally named as 'wp_srfm_entries'. * * @var string * @since 0.0.10 * @override */ protected $table_suffix; /** * Version for current custom table. Default is 1. * Unlike semantic versioning [eg: 1.0.0, 1.0.1] we use natural integer like 1, 2, 3... and so on. * Update the table version from child class when any DB upgrade or alteration related changes are made. * * @var int * @since 0.0.13 * @override */ protected $table_version = 1; /** * Full table name mixed with table prefix and table suffix. * * @var string * @since 0.0.10 */ private $table_name; /** * Whether or not the current database table is upgradable. * Determines on the basis of the table version. * * @var bool * @since 0.0.13 */ private $db_upgradable; /** * Current table database result caches. * * @var array
* @since 0.0.10 */ private $caches = []; /** * Init class. * * @since 0.0.10 * @return void */ public function __construct() { global $wpdb; $this->wpdb = $wpdb; $this->table_prefix = $this->wpdb->prefix . 'srfm_'; $this->table_name = $this->table_prefix . $this->table_suffix; } /** * Actions to initialize during object unload. * * @since 0.0.13 * @return void */ public function __destruct() { /** * Just incase if any developer forgets to stop the db upgrade after starting. * This fallback handling will take care of such scenarios. */ $this->stop_db_upgrade(); } /** * Returns the current table schema. * * @since 0.0.10 * @return array
> */ abstract public function get_schema(); /** * Current table columns definition to create table. These definitions will be used by the create() method. * * @since 0.0.13 * @return array
*/ abstract public function get_columns_definition(); /** * Any columns that needs to be added if the current table already exists. These definitions will be used by maybe_add_new_columns() method. * Override this from child class if needed. * * @since 0.0.13 * @return array
* @override */ public function get_new_columns_definition() { return []; } /** * Array of columns that needs to be renamed to new column name. It will be used by maybe_rename_columns() method. * Format: * [ * [ * 'from' => 'old_column_name', * 'to' => 'new_column_name', * 'type' => 'column type definition eg: LONGTEXT', // Optional. * ], * ] * * @since 0.0.13 * @return array
> */ public function get_columns_to_rename() { return []; } /** * Start the database upgrade process. * * @since 0.0.13 * @return void */ public function start_db_upgrade() { $versions = Helper::get_array_value( get_option( 'srfm_database_table_versions', [] ) ); $prev_version = ! empty( $versions[ $this->table_suffix ] ) ? absint( $versions[ $this->table_suffix ] ) : false; if ( ! $prev_version ) { /** * If we are here then there is the chance that * this site is the new site or fresh setup. */ $this->db_upgradable = true; return; } $this->db_upgradable = $this->table_version > $prev_version; } /** * Stop the database upgrade process. * * @since 0.0.13 * @return bool Returns true on success. */ public function stop_db_upgrade() { if ( ! $this->db_upgradable ) { // Only upgrade when it is needed. return false; } $versions = Helper::get_array_value( get_option( 'srfm_database_table_versions', [] ) ); $versions[ $this->table_suffix ] = $this->table_version; update_option( 'srfm_database_table_versions', $versions ); return true; } /** * Check if current table's DB is upgradable or not. * * @since 0.0.13 * @return bool True or false depending if DB is upgradable or not. */ public function is_db_upgradable() { return $this->db_upgradable; } /** * Returns full table name. * * @since 0.0.10 * @return string */ public function get_tablename() { return $this->table_name; } /** * Conditionally returns current database charset or collate. * * @since 0.0.10 * @return string */ public function get_charset_collate() { $charset_collate = ''; if ( $this->wpdb->has_cap( 'collation' ) ) { if ( ! empty( $this->wpdb->charset ) ) { $charset_collate = "DEFAULT CHARACTER SET {$this->wpdb->charset}"; } if ( ! empty( $this->wpdb->collate ) ) { $charset_collate .= " COLLATE {$this->wpdb->collate}"; } } return $charset_collate; } /** * Create table. * * @param array
$columns Array of columns. * @since 0.0.10 * @return int|bool */ public function create( $columns = [] ) { if ( ! $this->db_upgradable ) { // Only upgrade when it is needed. return false; } if ( empty( $columns ) ) { return false; // It's better to return a boolean for failure. } // Prepare columns list. $columns_list = implode( ', ', $columns ); $wpdb = $this->wpdb; // Execute the query. $query = $wpdb->prepare( 'CREATE TABLE IF NOT EXISTS %1s ( %2s ) %3s', $this->get_tablename(), $columns_list, $this->get_charset_collate() ); // phpcs:ignore -- It is okay to use complex placeholder here for the table name, column list and character set because we don't want to quote these variables. if ( ! $query ) { // If we are here, then we probably have bad query to work with and prepare method has returned null-ish value. return false; } $result = $wpdb->query( $query ); // phpcs:ignore -- We are already using prepare above. if ( false === $result ) { // Stop DB alteration if we have any error. $this->db_upgradable = false; } return $result; } /** * Rename the column of the current table conditionally. * * @param array
> $rename_columns Array of columns to rename. * @since 0.0.13 * @return int|bool Boolean true for CREATE, ALTER, TRUNCATE and DROP queries. Number of rows affected/selected for all other queries. Boolean false on error. */ public function maybe_rename_columns( $rename_columns = [] ) { if ( ! $rename_columns ) { return false; } if ( ! $this->db_upgradable ) { // Only upgrade when it is needed. return false; } $existing_columns = $this->get_columns(); if ( ! $existing_columns ) { // Table does not exists or is new table. return false; } $wpdb = $this->wpdb; $query_parts = []; foreach ( $rename_columns as $column ) { if ( empty( $existing_columns[ $column['from'] ] ) ) { // Bail if column is already renamed or does not exists. continue; } $query_part = $wpdb->prepare( 'CHANGE %1s %2s %3s', // phpcs:ignore -- It is okay to use complex placeholders as we don't want values to be quoted. $column['from'], $column['to'], ! empty( $column['type'] ) ? $column['type'] : $existing_columns[ $column['from'] ]['Type'] // This is column type i.e LONGTEXT, BIGINT etc. ); if ( is_string( $query_part ) && $query_part ) { $query_parts[] = trim( $query_part ); } } if ( empty( $query_parts ) ) { // No renaming required. return false; } $result = $wpdb->query( $wpdb->prepare( 'ALTER TABLE %1s ', $this->get_tablename() ) . implode( ', ', $query_parts ) . ';' ); // phpcs:ignore -- It is okay to use query directly here. if ( false === $result ) { // Stop DB alteration if we have any error. $this->db_upgradable = false; } return $result; } /** * Adds the new columns to the current table conditionally. * * @param array
$new_columns The array of new columns to add. Same as the create method. * @since 0.0.13 * @return int|bool Boolean true for CREATE, ALTER, TRUNCATE and DROP queries. Number of rows affected/selected for all other queries. Boolean false on error. */ public function maybe_add_new_columns( $new_columns = [] ) { if ( ! $new_columns ) { return false; } if ( ! $this->db_upgradable ) { // Only upgrade when it is needed. return false; } $existing_columns = $this->get_columns(); if ( ! $existing_columns ) { // Table does not exists or is new table. return false; } $existing_indexes = $this->get_indexes(); $alter_queries = []; $wpdb = $this->wpdb; // Check and add each column if it does not exist. foreach ( $new_columns as $column_definition ) { preg_match( '/INDEX\s+(.*?)\s+\(/', $column_definition, $index_matches ); if ( ! empty( $index_matches[1] ) ) { if ( isset( $existing_indexes[ $index_matches[1] ] ) ) { // Move to next element if current index already exists. continue; } // Stack and move to next if we are indexing. $alter_queries[] = $wpdb->prepare( 'ADD %1s', $column_definition ); // phpcs:ignore -- We don't need quote here. continue; } preg_match( '/(\w+)\s/', $column_definition, $column_matches ); $column_name = $column_matches[1]; // If the column does not exist, add it. if ( ! isset( $existing_columns[ $column_name ] ) ) { $alter_queries[] = $wpdb->prepare( 'ADD COLUMN %1s', $column_definition ); // phpcs:ignore -- We don't need quote here. } } if ( $alter_queries ) { $query = $wpdb->prepare( 'ALTER TABLE %1s %2s', // phpcs:ignore -- We don't want to quote the value strings for the query. $this->get_tablename(), implode( ', ', $alter_queries ) ); if ( ! $query ) { // If we are here then we probably have bad query and prepare method has returned null. return false; } // Execute the query. $result = $wpdb->query( $query ); // phpcs:ignore -- It is okay. We are already using prepare above and we need to do DB query directly here. if ( false === $result ) { // Stop DB alteration if we have any error. $this->db_upgradable = false; } return $result; } return false; } /** * Returns an array columns of current table. * * @since 0.0.13 * @return array
> */ public function get_columns() { $wpdb = $this->wpdb; $columns = $wpdb->get_results( $wpdb->prepare( 'SHOW COLUMNS FROM %1s', $this->get_tablename() ), ARRAY_A ); // phpcs:ignore -- It is okay to use query db directly here. if ( empty( $columns ) ) { return []; } $_columns = []; if ( is_array( $columns ) ) { foreach ( $columns as $column ) { if ( ! is_string( $column['Field'] ) ) { continue; } $_columns[ $column['Field'] ] = $column; } } return $_columns; } /** * Returns an array indexes of current table. * * @since 0.0.13 * @return array
*/ public function get_indexes() { $wpdb = $this->wpdb; $indexes = $wpdb->get_results( $wpdb->prepare( 'SHOW INDEX FROM %1s', $this->get_tablename() ), ARRAY_A ); // phpcs:ignore -- We don't need quote here so this is fine. if ( empty( $indexes ) ) { return []; } $_indexes = []; if ( is_array( $indexes ) ) { foreach ( $indexes as $index ) { $_indexes[ $index['Key_name'] ] = $index; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } } return $_indexes; } /** * Insert data. Basically, a wrapper method for wpdb::insert. * * @param array
$data Data to insert (in column => value pairs). * Both `$data` columns and `$data` values should be "raw" (neither should be SQL escaped). * Sending a null value will cause the column to be set to NULL - the corresponding * format is ignored in this case. * @param array
|string|null $format Optional. An array of formats to be mapped to each of the value in `$data`. * If string, that format will be used for all of the values in `$data`. * A format is one of '%d', '%f', '%s' (integer, float, string). * If omitted, all values in `$data` will be treated as strings unless otherwise * specified in wpdb::$field_types. Default null. * @since 0.0.10 * @return int|false The id of the inserted entry, or false on error. */ public function use_insert( $data, $format = null ) { $prepared_data = $this->prepare_data( $data ); if ( is_null( $format ) ) { /** * Use formats from schema if not provided explicitly. * * @var array
|string|null $format Format specifier for the data. */ $format = $prepared_data['format']; } $result = $this->wpdb->insert( $this->get_tablename(), $prepared_data['data'], $format ); return $result ? $this->wpdb->insert_id : false; } /** * Update a row data of current table. Basically, a wrapper method for wpdb::update. * * @param array
$data Data to update (in column => value pairs). * Both $data columns and $data values should be "raw" (neither should be SQL escaped). * Sending a null value will cause the column to be set to NULL - the corresponding * format is ignored in this case. * @param array
$where A named array of WHERE clauses (in column => value pairs). * Multiple clauses will be joined with ANDs. * Both $where columns and $where values should be "raw". * Sending a null value will create an IS NULL comparison - the corresponding * format will be ignored in this case. * @since 0.0.13 * @return int|false The number of rows updated, or false on error. */ public function use_update( $data, $where ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- It is okay. This is our wrapper method. $prepared_data = $this->prepare_data( $data, true ); /** * Data format specifier. * * @var array
|string|null $format Format specifier for the data. */ $format = $prepared_data['format']; return $this->wpdb->update( $this->get_tablename(), $data, $where, $format ); } /** * Delete a row data of current table. Basically, a wrapper method for wpdb::delete. * * @param array
$where A named array of WHERE clauses (in column => value pairs). * Multiple clauses will be joined with ANDs. * Both $where columns and $where values should be "raw". * Sending a null value will create an IS NULL comparison - the corresponding * format will be ignored in this case. * @param array
|string $where_format Optional. An array of formats to be mapped to each of the values in $where. * If string, that format will be used for all of the items in $where. * A format is one of '%d', '%f', '%s' (integer, float, string). * If omitted, all values in $data will be treated as strings unless otherwise * specified in wpdb::$field_types. Default null. * @since 0.0.13 * @return int|false The number of rows deleted, or false on error. */ public function use_delete( $where, $where_format = null ) { return $this->wpdb->delete( $this->get_tablename(), $where, $where_format ); } /** * Retrieve results from the database based on the given WHERE clauses and selected columns. * * This method builds a SQL SELECT query with optional WHERE clauses and retrieves the results * from the database. The results are cached to improve performance on subsequent requests. * * @param array
$where_clauses Optional. An associative array of WHERE clauses for the SQL query. * Each key represents a column name, and each value is the value * to match. If the value is an array, it will be used in an IN clause. * Example: ['column1' => 'value1', 'column2' => ['value2', 'value3']]. * Default is an empty array. * @param string $columns Optional. A string specifying which columns to select. Defaults to '*' (all columns). * @param array
$extra_queries Optional. Array of extra queries to append at the end of main query. * @param bool $decode Optional. Whether to decode the results by datatype. Default is true. * @since 0.0.10 * @return array
An associative array of results where each element represents a row, or an empty array if no results are found. */ public function get_results( $where_clauses = [], $columns = '*', $extra_queries = [], $decode = true ) { $wpdb = $this->wpdb; $table_name = $this->get_tablename(); // Start building the query. $query = "SELECT {$columns} FROM {$table_name}"; // If there are WHERE clauses, prepare and append them to the query. $query .= $this->prepare_where_clauses( $where_clauses ); if ( ! empty( $extra_queries ) ) { $query .= ' ' . implode( ' ', array_map( 'trim', $extra_queries ) ); } // Add a semicolon at the end of the query. $query = rtrim( trim( $query ), ';' ) . ';'; $cached_results = $this->cache_get( $query ); if ( $cached_results ) { // Return the cached data if exists. return Helper::get_array_value( $cached_results ); } // phpcs:ignore $results = $wpdb->get_results( $query, ARRAY_A ); if ( $decode && ! empty( $results ) && is_array( $results ) ) { foreach ( $results as &$result ) { $result = $this->decode_by_datatype( $result ); } } // Execute the query and return results. return Helper::get_array_value( $this->cache_set( $query, $results ) ); } /** * Get the total number of rows in the table. * * @param array
$where_clauses Optional. An associative array of WHERE clauses for the SQL query. * @since 0.0.13 * @return int The total number of rows in the table. */ public function get_total_count( $where_clauses = [] ) { $wpdb = $this->wpdb; $table_name = $this->get_tablename(); // Start building the query. $query = "SELECT COUNT(*) FROM {$table_name}"; // If there are WHERE clauses, prepare and append them to the query. $query .= $this->prepare_where_clauses( $where_clauses ); // Add a semicolon at the end of the query. $query = rtrim( trim( $query ), ';' ) . ';'; $cached_results = $this->cache_get( $query ); if ( $cached_results ) { // Return the cached data if exists. return Helper::get_integer_value( $cached_results ); } // phpcs:ignore $results = Helper::get_integer_value( $wpdb->get_var( $query ) ); // Execute the query and return the integer count. return Helper::get_integer_value( $this->cache_set( $query, $results ) ); } /** * Retrieve a cached value by its key. * * @param string $key The cache key. * @since 0.0.10 * @return mixed|null The cached value if it exists, or null if the key does not exist in the cache. */ protected function cache_get( $key ) { $key = md5( $key ); if ( ! isset( $this->caches[ $key ] ) ) { return null; } return $this->caches[ $key ]; } /** * Store a value in the cache with the specified key. * * @param string $key The cache key. * @param mixed $value The value to store in the cache. * @since 0.0.10 * @return mixed The stored value. */ protected function cache_set( $key, $value ) { $key = md5( $key ); $this->caches[ $key ] = $value; return $value; } /** * Reset the cache by clearing all stored values. * * @since 0.0.10 * @return void */ protected function cache_reset() { $this->caches = []; } /** * Prepares WHERE clauses for a SQL query based on the provided conditions. * * This method constructs a WHERE statement by iterating through the * specified conditions, appending them with the appropriate SQL syntax. * It supports both single key-value pairs and arrays of conditions. * * @param array
$where_clauses { * An associative array of conditions to include in the WHERE clause. * * @type string|array $key The column name or an array of conditions. * @type array $value { * An associative array of comparison data. * * @type string $key The column name for comparison. * @type string $compare The comparison operator (e.g., '=', 'LIKE'). * @type mixed $value The value to compare against. * @type string $RELATION Optional. The logical relation ('AND' or 'OR'). * } * } * * @since 1.1.1 -- Added support for "IN" compare. * @since 0.0.13 * @return string The prepared SQL WHERE clause with placeholders, or an empty string if no clauses were provided. */ protected function prepare_where_clauses( $where_clauses = [] ) { if ( empty( $where_clauses ) ) { return ''; } $wpdb = $this->wpdb; // If there are WHERE clauses, prepare and append them to the query. if ( is_array( $where_clauses ) ) { $where = ''; $values = []; $schema = $this->get_schema(); foreach ( $where_clauses as $key => $value ) { $relation = ! empty( $value['RELATION'] ) ? trim( $value['RELATION'] ) : 'AND'; if ( is_int( $key ) ) { foreach ( $value as $_key => $_value ) { if ( is_int( $_key ) ) { switch ( $_value['compare'] ) { case 'LIKE': $where .= ' ' . $_value['key'] . ' ' . $_value['compare'] . ' "%%' . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ) . '%%" ' . $relation; $values[] = $_value['value']; break; case 'IN': // Based on the number of values and datatype, it will create WHERE clause for $wpdb::prepare method. Eg: for ID with three values column: ID IN (%d, %d, %d). $datatype = $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ); $where .= ' ' . $_value['key'] . ' ' . $_value['compare'] . ' (' . implode( ', ', array_fill( 0, count( $_value['value'] ), $datatype ) ) . ') ' . $relation; $values = array_merge( $values, $_value['value'] ); break; default: $where .= ' ' . $_value['key'] . ' ' . $_value['compare'] . ' ' . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ) . ' ' . $relation; $values[] = $_value['value']; break; } } } continue; } if ( ! isset( $schema[ $key ] ) ) { // Skip strictly if current key is not in our schema. continue; } $where .= ' ' . $key . ' = ' . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $key ]['type'] ) ) . ' ' . $relation; $values[] = $value; } if ( ! $where ) { return ''; } $where = ' WHERE ' . trim( trim( $where, $relation ) ); // Prepare the query with placeholders. // @phpstan-ignore-next-line -- We are already assigning non-literal string above using "get_format_by_datatype" methods. return $wpdb->prepare( $where, ...$values ); // phpcs:ignore -- We are returning prepared sql query here. We are already using necessary placeholders in $where variable. } return ''; } /** * Prepare and format data based on the schema. * * @param array
$data An associative array of data where the key is the column name and the value is the data to process. * Missing values will be replaced with default values specified in the schema. * @param bool $skip_defaults Whether or not to skip the defaults values. Pass true if updating the data. * @since 0.0.10 * @return array
> An associative array containing: * - 'data': Prepared data with values encoded according to their data types. * - 'format': An array of format specifiers corresponding to the data values. */ protected function prepare_data( $data, $skip_defaults = false ) { $_data = []; $format = []; foreach ( $this->get_schema() as $key => $value ) { // Process defaults. if ( ! isset( $data[ $key ] ) ) { if ( $skip_defaults || ! isset( $value['default'] ) ) { continue; } $data[ $key ] = $value['default']; } $format[] = $this->get_format_by_datatype( $value['type'] ); // Format for the WP database methods. $_data[ $key ] = $this->encode_by_datatype( $data[ $key ], $value['type'] ); } return [ 'data' => $_data, 'format' => $format, ]; } /** * Get the SQL format specifier based on the provided data type. * * @param string $type The data type for which to get the SQL format specifier. * Possible values: 'string', 'array', 'number', 'boolean'. * @since 0.0.10 * @return string The SQL format specifier. One of '%s' for string or array (converted to JSON), '%d' for number or boolean. */ protected function get_format_by_datatype( $type ) { $format = '%s'; switch ( $type ) { case 'string': case 'array': // Because array will be converted to json string. $format = '%s'; break; case 'number': case 'boolean': $format = '%d'; break; } return $format; } /** * Decode data based on the schema data types. * * @param array
$data An associative array of data where the key is the column name and the value is the data to decode. * The data will be decoded if the column type in the schema is 'array' (JSON string). * @since 0.0.10 * @return array
An associative array of decoded data based on the schema. */ protected function decode_by_datatype( $data ) { $_data = []; foreach ( $this->get_schema() as $key => $schema ) { if ( ! array_key_exists( $key, $data ) ) { continue; } // Lets decode from JSON to Array for the results. $_data[ $key ] = 'array' === $schema['type'] ? Helper::get_array_value( json_decode( Helper::get_string_value( $data[ $key ] ), true ) ) : $data[ $key ]; } return $_data; } /** * Encode a value based on the specified data type. * * @param mixed $value The value to encode. The encoding will depend on the data type specified. * @param string $type The data type for encoding. Possible values: 'string', 'number', 'boolean', 'array'. * @since 0.0.10 * @return mixed The encoded value. The type of the return value depends on the specified type: * - 'string': Encoded as a string. * - 'number': Encoded as an integer. * - 'boolean': Encoded as a boolean. * - 'array': Encoded as a JSON string. */ protected function encode_by_datatype( $value, $type ) { switch ( $type ) { case 'string': return Helper::get_string_value( $value ); case 'number': return Helper::get_integer_value( $value ); case 'boolean': return boolval( $value ); case 'array': // Lets json_encode array values instead of serializing it. return Helper::encode_json( Helper::get_array_value( $value ) ); } } }