diff --git a/package.json b/package.json index f523c9b..1e9f9f0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "autowpdb", "description": "Create and use custom database tables in WordPress.", - "version": "0.1.0", + "version": "0.2.0", "homepage": "https://github.com/Screenfeed/autowpdb", "license": "GPL-2.0", "private": true, diff --git a/phpcs.xml b/phpcs.xml index 637c2cf..21c3ffb 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -24,6 +24,7 @@ + diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 02e3e9a..db1f6f4 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -6,3 +6,5 @@ parameters: inferPrivatePropertyTypeFromConstructor: true paths: - %currentWorkingDirectory%/src/ + ignoreErrors: + - '#^Function apply_filters(_ref_array)? invoked with \d parameters, \d required\.$#' diff --git a/src/CRUD/AbstractCRUD.php b/src/CRUD/AbstractCRUD.php index 0ccc399..8a6311d 100644 --- a/src/CRUD/AbstractCRUD.php +++ b/src/CRUD/AbstractCRUD.php @@ -19,6 +19,10 @@ * Abstract class that contains some tools to help interacting with the DB table. * * @since 0.1 + * @uses $GLOBALS['wpdb'] + * @uses esc_sql() + * @uses maybe_unserialize() + * @uses maybe_serialize() */ abstract class AbstractCRUD implements CRUDInterface { @@ -28,7 +32,7 @@ abstract class AbstractCRUD implements CRUDInterface { * @var TableDefinitionInterface * @since 0.1 */ - protected $table; + private $table_definition; /** * Stores the list of columns that must be (un)serialized. @@ -60,12 +64,12 @@ abstract class AbstractCRUD implements CRUDInterface { * * @since 0.1 * - * @param TableDefinitionInterface $table A TableDefinitionInterface object. + * @param TableDefinitionInterface $table_definition A TableDefinitionInterface object. */ - public function __construct( TableDefinitionInterface $table ) { + public function __construct( TableDefinitionInterface $table_definition ) { global $wpdb; - $this->table = $table; + $this->table_definition = $table_definition; } /** ----------------------------------------------------------------------------------------- */ @@ -73,14 +77,14 @@ public function __construct( TableDefinitionInterface $table ) { /** ----------------------------------------------------------------------------------------- */ /** - * Get the table. + * Get the TableDefinitionInterface object. * - * @since 0.1 + * @since 0.2 * * @return TableDefinitionInterface */ - public function get_table(): TableDefinitionInterface { - return $this->table; + public function get_table_definition(): TableDefinitionInterface { + return $this->table_definition; } /** ----------------------------------------------------------------------------------------- */ @@ -107,7 +111,7 @@ protected function prepare_select_for_query( array $select ) { // phpcs:ignore N return '*'; } - $column_names = array_keys( $this->table->get_column_placeholders() ); + $column_names = array_keys( $this->table_definition->get_column_placeholders() ); $select = array_map( 'strtolower', $select ); $select = array_intersect( $select, $column_names ); @@ -136,7 +140,7 @@ protected function prepare_data_for_query( array $data ): array { $data = array_change_key_case( $data ); // Keep only valid columns. - $data = array_intersect_key( $data, $this->table->get_column_placeholders() ); + $data = array_intersect_key( $data, $this->table_definition->get_column_placeholders() ); // Maybe serialize some values. return $this->serialize_columns( $data ); @@ -154,7 +158,7 @@ protected function prepare_data_for_query( array $data ): array { * @return array */ protected function get_placeholders( array $columns ): array { - $formats = $this->table->get_column_placeholders(); + $formats = $this->table_definition->get_column_placeholders(); $formats = array_intersect_key( $formats, $columns ); return array_merge( $columns, $formats ); @@ -170,8 +174,8 @@ protected function get_placeholders( array $columns ): array { * @return string */ protected function get_placeholder( string $column ): string { - $columns = $this->table->get_column_placeholders(); - return isset( $columns[ $column ] ) ? $columns[ $column ] : '%s'; + $columns = $this->table_definition->get_column_placeholders(); + return $columns[ $column ] ?? '%s'; } /** @@ -183,8 +187,8 @@ protected function get_placeholder( string $column ): string { * @return mixed|null The default value. Null if the column does not exist. */ protected function get_default_value( string $column ) { // phpcs:ignore NeutronStandard.Functions.TypeHint.NoReturnType - $columns = $this->table->get_column_defaults(); - return isset( $columns[ $column ] ) ? $columns[ $column ] : null; + $columns = $this->table_definition->get_column_defaults(); + return $columns[ $column ] ?? null; } /** @@ -276,7 +280,7 @@ protected function cast_row( $row_fields ) { // phpcs:ignore NeutronStandard.Fun protected function serialize_columns( array $data ): array { if ( ! isset( $this->to_serialize ) ) { $this->to_serialize = array_filter( - $this->table->get_column_defaults(), + $this->table_definition->get_column_defaults(), function ( $value ): bool { // phpcs:ignore NeutronStandard.Functions.TypeHint.NoArgumentType return is_array( $value ) || is_object( $value ); } @@ -330,7 +334,7 @@ protected function get_auto_increment_columns(): array { return $this->auto_increment_columns; } - $schema = $this->table->get_table_schema(); + $schema = $this->table_definition->get_table_schema(); if ( preg_match_all( '@^\s*(?[^\s]+)\s.+\sauto_increment,?$@mi', $schema, $matches ) ) { $this->auto_increment_columns = array_fill_keys( $matches['col_name'], '' ); diff --git a/src/CRUD/Basic.php b/src/CRUD/Basic.php index 7e43e7f..b7f6845 100644 --- a/src/CRUD/Basic.php +++ b/src/CRUD/Basic.php @@ -39,13 +39,13 @@ public function insert( array $data ): int { $data = $this->prepare_data_for_query( $data ); // Add default values to missing fields. - $data = array_merge( $this->serialize_columns( $this->table->get_column_defaults() ), $data ); + $data = array_merge( $this->serialize_columns( $this->get_table_definition()->get_column_defaults() ), $data ); // Remove the auto-increment columns. $data = array_diff_key( $data, $this->get_auto_increment_columns() ); $wpdb->insert( - $this->table->get_table_name(), + $this->get_table_definition()->get_table_name(), $data, $this->get_placeholders( $data ) ); @@ -69,10 +69,10 @@ public function replace( array $data ): int { $data = $this->prepare_data_for_query( $data ); // Add default values to missing fields. - $data = array_merge( $this->serialize_columns( $this->table->get_column_defaults() ), $data ); + $data = array_merge( $this->serialize_columns( $this->get_table_definition()->get_column_defaults() ), $data ); $wpdb->replace( - $this->table->get_table_name(), + $this->get_table_definition()->get_table_name(), $data, $this->get_placeholders( $data ) ); @@ -108,7 +108,7 @@ public function get( array $select, array $where, string $output_type = OBJECT ) return null; } - $table = $this->table->get_table_name(); + $table = $this->get_table_definition()->get_table_name(); $where = $this->prepare_data_for_query( $where ); if ( ! empty( $where ) ) { @@ -182,7 +182,7 @@ public function update( array $data, array $where ) { // phpcs:ignore NeutronSta $where = $this->prepare_data_for_query( $where ); return $wpdb->update( - $this->table->get_table_name(), + $this->get_table_definition()->get_table_name(), $data, $where, $this->get_placeholders( $data ), @@ -213,7 +213,7 @@ public function delete( array $where ) { // phpcs:ignore NeutronStandard.Functio $where = $this->prepare_data_for_query( $where ); return $wpdb->delete( - $this->table->get_table_name(), + $this->get_table_definition()->get_table_name(), $where, $this->get_placeholders( $where ) ); diff --git a/src/CRUD/CRUDInterface.php b/src/CRUD/CRUDInterface.php index 747d1ca..1bf09fc 100644 --- a/src/CRUD/CRUDInterface.php +++ b/src/CRUD/CRUDInterface.php @@ -26,13 +26,13 @@ interface CRUDInterface { /** ----------------------------------------------------------------------------------------- */ /** - * Get the table. + * Get the TableDefinitionInterface object. * - * @since 0.1 + * @since 0.2 * * @return TableDefinitionInterface */ - public function get_table(): TableDefinitionInterface; + public function get_table_definition(): TableDefinitionInterface; /** ----------------------------------------------------------------------------------------- */ /** CREATE ================================================================================== */ diff --git a/src/DBUtilities.php b/src/DBUtilities.php index bc828b5..f32a3a1 100644 --- a/src/DBUtilities.php +++ b/src/DBUtilities.php @@ -16,47 +16,27 @@ * * @since 0.1 * @uses $GLOBALS['wpdb'] + * @uses ABSPATH + * @uses WP_DEBUG + * @uses WP_DEBUG_LOG * @uses dbDelta() + * @uses esc_sql() + * @uses remove_accents() + * @uses sanitize_key() */ class DBUtilities { - /** - * Change an array of values into a comma separated list, ready to be used in a `IN ()` clause. - * - * @since 0.1 - * - * @param array $values An array of values. - * @return string A comma separated list of values. - */ - public static function prepare_values_list( array $values ): string { - $values = esc_sql( (array) $values ); - $values = array_map( [ __CLASS__, 'quote_string' ], $values ); - return implode( ',', $values ); - } - - /** - * Wrap a value in quotes, unless it is a numeric value. - * - * @since 0.1 - * - * @param mixed $value A value. - * @return mixed - */ - public static function quote_string( $value ) { // phpcs:ignore NeutronStandard.Functions.TypeHint.NoArgumentType, NeutronStandard.Functions.TypeHint.NoReturnType - return is_numeric( $value ) || ! is_string( $value ) ? $value : "'" . addcslashes( $value, "'" ) . "'"; - } - /** * Create/Upgrade a table in the database. * * @since 0.1 * - * @param string $table_name The (prefixed) table name. + * @param string $table_name The (prefixed) table name. Use `sanitize_table_name()` before passing it to this method. * @param string $schema_query Query representing the table schema. - * @param array $args { + * @param array $args { * Optional arguments. * - * @var callable $logger Callback to use to log errors. The error message is passed to the callback as 1st argument. Default is 'error_log'. + * @var callable|false|null $logger Callback to use to log errors. The error message is passed to the callback as 1st argument. False to disable log. Null will default to 'error_log'. * } * @return bool True on success. False otherwise. */ @@ -67,20 +47,24 @@ public static function create_table( string $table_name, string $schema_query, a $wpdb->hide_errors(); - $logger = isset( $args['logger'] ) ? $args['logger'] : 'error_log'; + $logger = $args['logger'] ?? 'error_log'; $charset_collate = $wpdb->get_charset_collate(); - dbDelta( "CREATE TABLE $table_name ($schema_query) $charset_collate;" ); + dbDelta( "CREATE TABLE `$table_name` ($schema_query) $charset_collate;" ); if ( ! empty( $wpdb->last_error ) ) { // The query returned an error. - empty( $logger ) || call_user_func( $logger, sprintf( 'Error while creating the DB table %s: %s', $table_name, $wpdb->last_error ) ); + if ( static::can_log( $logger ) ) { + call_user_func( $logger, sprintf( 'Error while creating the DB table %s: %s', $table_name, $wpdb->last_error ) ); + } return false; } if ( ! self::table_exists( $table_name ) ) { - // The table does not exists (wtf). - empty( $logger ) || call_user_func( $logger, sprintf( 'Creation of the DB table %s failed.', $table_name ) ); + // The table does not exist (wtf). + if ( static::can_log( $logger ) ) { + call_user_func( $logger, sprintf( 'Creation of the DB table %s failed.', $table_name ) ); + } return false; } @@ -92,19 +76,262 @@ public static function create_table( string $table_name, string $schema_query, a * * @since 0.1 * - * @param string $table_name Full name of the table (with DB prefix). + * @param string $table_name Full name of the table (with DB prefix). Use `sanitize_table_name()` before passing it to this method. * @return bool */ public static function table_exists( string $table_name ): bool { global $wpdb; - $result = $wpdb->get_var( - $wpdb->prepare( - 'SHOW TABLES LIKE %s', - $wpdb->esc_like( $table_name ) - ) - ); + $table_name = $wpdb->esc_like( $table_name ); + $query = "SHOW TABLES LIKE `$table_name`"; + $result = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + return ( $result === $table_name ); + } + + /** + * Delete the given table (DROP). + * + * @since 0.2 + * @source inspired from https://github.com/berlindb/core/blob/734f799e04a9ce86724f2d906b1a6e0fc56fdeb4/table.php#L404-L427. + * + * @param string $table_name Full name of the table (with DB prefix). Use `sanitize_table_name()` before passing it to this method. + * @param array $args { + * Optional arguments. + * + * @var callable|false|null $logger Callback to use to log errors. The error message is passed to the callback as 1st argument. False to disable log. Null will default to 'error_log'. + * } + * @return bool True on success. False otherwise. + */ + public static function delete_table( string $table_name, array $args = [] ): bool { + global $wpdb; + + $logger = $args['logger'] ?? 'error_log'; + + $query = "DROP TABLE `$table_name`"; + $result = $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + if ( true !== $result || self::table_exists( $table_name ) ) { + // The table still exists. + if ( static::can_log( $logger ) ) { + call_user_func( $logger, sprintf( 'Deletion of the DB table %s failed.', $table_name ) ); + } + return false; + } + + return true; + } + + /** + * Reinit the given table (TRUNCATE): + * - Delete all entries, + * - Reinit auto-increment column. + * + * @since 0.2 + * @source Inspired from https://github.com/berlindb/core/blob/734f799e04a9ce86724f2d906b1a6e0fc56fdeb4/table.php#L429-L452. + * + * @param string $table_name Full name of the table (with DB prefix). Use `sanitize_table_name()` before passing it to this method. + * @return bool True on success. False otherwise. + */ + public static function reinit_table( string $table_name ): bool { + global $wpdb; + + $query = "TRUNCATE TABLE `$table_name`"; + $result = $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + return true === $result; + } + + /** + * Delete all rows from the given table (DELETE FROM): + * - Delete all entries, + * - Do NOT reinit auto-increment column, + * - Return the number of deleted entries, + * - Less performant than reinit. + * + * @since 0.2 + * @source Inspired from https://github.com/berlindb/core/blob/734f799e04a9ce86724f2d906b1a6e0fc56fdeb4/table.php#L454-L477. + * + * @param string $table_name Full name of the table (with DB prefix). Use `sanitize_table_name()` before passing it to this method. + * @return int Number of deleted rows. + */ + public static function empty_table( string $table_name ): int { + global $wpdb; + + $query = "DELETE FROM `$table_name`"; + + return (int) $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Clone the given table (without its contents). + * + * @since 0.2 + * @source Inspired from https://github.com/berlindb/core/blob/master/table.php#L479-L515. + * + * @param string $table_name Full name of the table (with DB prefix). Use `sanitize_table_name()` before passing it to this method. + * @param string $new_table_name Full name of the new table (with DB prefix). Use `sanitize_table_name()` before passing it to this method. + * @return bool True on success. False otherwise. + */ + public static function clone_table( string $table_name, string $new_table_name ): bool { + global $wpdb; + + $query = "CREATE TABLE `$new_table_name` LIKE `$table_name`"; + $result = $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + return true === $result; + } + + /** + * Copy the contents of the given table to a new table. + * + * @since 0.2 + * @source Inspired from https://github.com/berlindb/core/blob/734f799e04a9ce86724f2d906b1a6e0fc56fdeb4/table.php#L517-L553. + * + * @param string $table_name Full name of the table (with DB prefix). Use `sanitize_table_name()` before passing it to this method. + * @param string $new_table_name Full name of the new table (with DB prefix). Use `sanitize_table_name()` before passing it to this method. + * @return int Number of inserted rows. + */ + public static function copy_table( string $table_name, string $new_table_name ): int { + global $wpdb; + + $query = "INSERT INTO `$new_table_name` SELECT * FROM `$table_name`"; - return $result === $table_name; + return (int) $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Count the number of rows in the given table. + * + * @since 0.2 + * @source Inspired from https://github.com/berlindb/core/blob/734f799e04a9ce86724f2d906b1a6e0fc56fdeb4/table.php#L555-L578. + * + * @param string $table_name Full name of the table (with DB prefix). Use `sanitize_table_name()` before passing it to this method. + * @param string $column Name of the column to use in `COUNT()`. Optional, default is `*`. + * @return int Number of rows. + */ + public static function count_table_rows( string $table_name, string $column = '*' ): int { + global $wpdb; + + $prefix = ''; + $column = trim( $column ); + + if ( preg_match( '@^DISTINCT\s+(?[^\s]+)$@i', $column, $matches ) ) { + $prefix = 'DISTINCT '; + $column = $matches['column']; + } + if ( '*' !== $column ) { + $column = trim( $column, '`\'"' ); + $column = sprintf( '%s`%s`', $prefix, esc_sql( $column ) ); + } + + $query = "SELECT COUNT($column) FROM `$table_name`"; + + return (int) $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Sanitize a table name string. + * Used to make sure that a table name value meets MySQL expectations. + * + * Applies the following formatting to a string: + * - Trim whitespace, + * - No accents, + * - No special characters, + * - No hyphens, + * - No double underscores, + * - No trailing underscores. + * + * @since 0.2 + * @source Inspired from https://github.com/berlindb/core/blob/4d3a93e6036302957523c4f435ea1a67fc632180/base.php#L193-L244. + * + * @param string $table_name The name of the database table. + * @return string|null Sanitized database table name. Null on error. + */ + public static function sanitize_table_name( string $table_name ) { // phpcs:ignore NeutronStandard.Functions.TypeHint.NoReturnType + if ( empty( $table_name ) ) { + return null; + } + + $table_name = trim( $table_name ); + + // Only non-accented table names (avoid truncation). + $table_name = remove_accents( $table_name ); + + // Only lowercase characters, hyphens, and dashes (avoid index corruption). + $table_name = sanitize_key( $table_name ); + + // Replace hyphens with single underscores. + $table_name = str_replace( '-', '_', $table_name ); + + // Single underscores only. + $table_name = preg_replace( '@_{2,}@', '_', $table_name ); + + if ( empty( $table_name ) ) { + return null; + } + + // Remove trailing underscores. + $table_name = trim( $table_name, '_' ); + + if ( empty( $table_name ) ) { + return null; + } + + return $table_name; + } + + /** + * Change an array of values into a comma separated list, ready to be used in a `IN ()` clause. + * + * @since 0.1 + * + * @param array $values An array of values. + * @return string A comma separated list of values. + */ + public static function prepare_values_list( array $values ): string { + $values = esc_sql( (array) $values ); + $values = array_map( [ __CLASS__, 'quote_string' ], $values ); + return implode( ',', $values ); + } + + /** + * Wrap a value in quotes, unless it is a numeric value. + * + * @since 0.1 + * + * @param mixed $value A value. + * @return mixed + */ + public static function quote_string( $value ) { // phpcs:ignore NeutronStandard.Functions.TypeHint.NoArgumentType, NeutronStandard.Functions.TypeHint.NoReturnType + return is_numeric( $value ) || ! is_string( $value ) ? $value : "'" . addcslashes( $value, "'" ) . "'"; + } + + /** + * Wrap a value in quotes, unless it is a numeric value. + * + * @since 0.2 + * + * @param callable|false $logger Callback to use to log errors. The error message is passed to the callback as 1st argument. False to disable log. + * @return bool + */ + protected static function can_log( $logger ): bool { // phpcs:ignore NeutronStandard.Functions.TypeHint.NoArgumentType + if ( empty( $logger ) ) { + return false; + } + + if ( ! defined( 'WP_DEBUG' ) || empty( WP_DEBUG ) ) { + return false; + } + + if ( ! defined( 'WP_DEBUG_LOG' ) || empty( WP_DEBUG_LOG ) ) { + return false; + } + + if ( ! is_callable( $logger ) ) { + return false; + } + + return true; } } diff --git a/src/Table.php b/src/Table.php new file mode 100644 index 0000000..ec8ee22 --- /dev/null +++ b/src/Table.php @@ -0,0 +1,173 @@ +table_definition = $table_definition; + } + + /** + * Get the TableDefinitionInterface object. + * + * @since 0.2 + * + * @return TableDefinitionInterface + */ + public function get_table_definition(): TableDefinitionInterface { + return $this->table_definition; + } + + /** + * Create/Upgrade the table in the database. + * + * @since 0.2 + * + * @param array $args { + * Optional arguments. + * + * @var callable|false|null $logger Callback to use to log errors. The error message is passed to the callback as 1st argument. False to disable log. Null will default to 'error_log'. + * } + * @return bool True on success. False otherwise. + */ + public function create( array $args = [] ): bool { + return DBUtilities::create_table( $this->table_definition->get_table_name(), $this->table_definition->get_table_schema(), $args ); + } + + /** + * Tell if the table exists. + * + * @since 0.2 + * + * @return bool + */ + public function exists(): bool { + return DBUtilities::table_exists( $this->table_definition->get_table_name() ); + } + + /** + * Delete the table (DROP). + * + * @since 0.2 + * + * @param array $args { + * Optional arguments. + * + * @var callable|false|null $logger Callback to use to log errors. The error message is passed to the callback as 1st argument. False to disable log. Null will default to 'error_log'. + * } + * @return bool True on success. False otherwise. + */ + public function delete( array $args = [] ): bool { + return DBUtilities::delete_table( $this->table_definition->get_table_name(), $args ); + } + + /** + * Reinit the table (TRUNCATE): + * - Delete all entries, + * - Reinit auto-increment column. + * + * @since 0.2 + * + * @return bool True on success. False otherwise. + */ + public function reinit(): bool { + return DBUtilities::reinit_table( $this->table_definition->get_table_name() ); + } + + /** + * Delete all rows from the table (DELETE FROM): + * - Delete all entries, + * - Do NOT reinit auto-increment column, + * - Return the number of deleted entries, + * - Less performant than reinit. + * + * @since 0.2 + * + * @return int Number of deleted rows. + */ + public function empty(): int { + return DBUtilities::empty_table( $this->table_definition->get_table_name() ); + } + + /** + * Clone the table (without its contents). + * + * @since 0.2 + * + * @param string $new_table_name Full name of the new table (with DB prefix). + * @return bool True on success. False otherwise. + */ + public function clone_to( string $new_table_name ): bool { + $new_table_name = DBUtilities::sanitize_table_name( $new_table_name ); + + if ( empty( $new_table_name ) ) { + return false; + } + + return DBUtilities::clone_table( $this->table_definition->get_table_name(), $new_table_name ); + } + + /** + * Copy the contents of the table to a new table. + * + * @since 0.2 + * + * @param string $new_table_name Full name of the new table (with DB prefix). + * @return int Number of inserted rows. + */ + public function copy_to( string $new_table_name ): int { + $new_table_name = DBUtilities::sanitize_table_name( $new_table_name ); + + if ( empty( $new_table_name ) ) { + return 0; + } + + return DBUtilities::copy_table( $this->table_definition->get_table_name(), $new_table_name ); + } + + /** + * Count the number of rows in the table. + * + * @since 0.2 + * + * @param string $column Name of the column to use in `COUNT()`. Optional, default is `*`. + * @return int Number of rows. + */ + public function count( string $column = '*' ): int { + return DBUtilities::count_table_rows( $this->table_definition->get_table_name(), $column ); + } +} diff --git a/src/TableDefinition/AbstractTableDefinition.php b/src/TableDefinition/AbstractTableDefinition.php index 6ab58bc..542faac 100644 --- a/src/TableDefinition/AbstractTableDefinition.php +++ b/src/TableDefinition/AbstractTableDefinition.php @@ -9,6 +9,9 @@ namespace Screenfeed\AutoWPDB\TableDefinition; +use JsonSerializable; +use Screenfeed\AutoWPDB\DBUtilities; + defined( 'ABSPATH' ) || exit; // @phpstan-ignore-line /** @@ -16,8 +19,18 @@ * * @since 0.1 * @uses $GLOBALS['wpdb'] + * @uses DBUtilities + * @uses wp_json_encode() */ -abstract class AbstractTableDefinition implements TableDefinitionInterface { +abstract class AbstractTableDefinition implements TableDefinitionInterface, JsonSerializable { + + /** + * The (prefixed) table name. + * + * @var string|null + * @since 0.1 + */ + protected $full_table_name; /** * Get the table name. @@ -29,8 +42,51 @@ abstract class AbstractTableDefinition implements TableDefinitionInterface { public function get_table_name(): string { global $wpdb; + if ( ! empty( $this->full_table_name ) ) { + return $this->full_table_name; + } + $prefix = $this->is_table_global() ? $wpdb->base_prefix : $wpdb->prefix; - return $prefix . $this->get_table_short_name(); + $this->full_table_name = $prefix . DBUtilities::sanitize_table_name( $this->get_table_short_name() ); + + return $this->full_table_name; + } + + /** + * Convert the current object to an array. + * + * @since 0.2 + * + * @return array Array representation of the current object. + */ + public function jsonSerialize(): array { + return [ + 'table_version' => $this->get_table_version(), + 'table_short_name' => $this->get_table_short_name(), + 'table_name' => $this->get_table_name(), + 'table_is_global' => $this->is_table_global(), + 'primary_key' => $this->get_primary_key(), + 'column_placeholders' => $this->get_column_placeholders(), + 'column_defaults' => $this->get_column_defaults(), + 'table_schema' => $this->get_table_schema(), + ]; + } + + /** + * Convert the current object to a string. + * + * @since 0.2 + * + * @return string String representation of the current object. An empty string on error. + */ + public function __toString(): string { + $string = wp_json_encode( $this ); + + if ( false === $string ) { + return ''; + } + + return $string; } } diff --git a/src/TableUpgrader.php b/src/TableUpgrader.php index 5a73fd9..a0fc074 100644 --- a/src/TableUpgrader.php +++ b/src/TableUpgrader.php @@ -9,7 +9,7 @@ namespace Screenfeed\AutoWPDB; -use Screenfeed\AutoWPDB\DBUtilities; +use Screenfeed\AutoWPDB\Table; use Screenfeed\AutoWPDB\TableDefinition\TableDefinitionInterface; defined( 'ABSPATH' ) || exit; // @phpstan-ignore-line @@ -18,7 +18,16 @@ * Class that creates or upgrades a table automatically. * * @since 0.1 - * @uses DBUtilities + * @uses add_action() + * @uses is_multisite() + * @uses get_site_option() + * @uses get_option() + * @uses update_site_option() + * @uses update_option() + * @uses delete_site_option() + * @uses delete_option() + * @uses wp_should_upgrade_global_tables() + * @uses apply_filters() */ class TableUpgrader { @@ -31,9 +40,9 @@ class TableUpgrader { const TABLE_VERSION_OPTION_SUFFIX = '_db_version'; /** - * A TableDefinitionInterface object. + * A Table object. * - * @var TableDefinitionInterface + * @var Table * @since 0.1 */ protected $table; @@ -62,6 +71,14 @@ class TableUpgrader { */ protected $upgrade_hook_prio; + /** + * Callback to use to log errors. The error message is passed to the callback as 1st argument. False to disable log. Null will default to 'error_log'. + * + * @var callable|false|null + * @since 0.2 + */ + protected $logger; + /** * Tell if the table is ready to be used. * @@ -75,25 +92,27 @@ class TableUpgrader { * * @since 0.1 * - * @param TableDefinitionInterface $table A TableDefinitionInterface object. - * @param array $args { + * @param Table $table A Table object. + * @param array $args { * Optional arguments. * - * @var bool $handle_downgrade Set to true to allow table downgrade. Default is false. - * @var string $upgrade_hook Name of the hook that will trigger the table creation/upgrade. Use an empty string to not create the hook. Default is 'admin_menu'. - * @var int $upgrade_hook_prio Priority for the hook that will trigger the table creation/upgrade. Default is 8. + * @var bool $handle_downgrade Set to true to allow table downgrade. Default is false. + * @var string $upgrade_hook Name of the hook that will trigger the table creation/upgrade. Use an empty string to not create the hook. Default is 'admin_menu'. + * @var int $upgrade_hook_prio Priority for the hook that will trigger the table creation/upgrade. Default is 8. + * @var callable|false $logger Callback to use to log errors. The error message is passed to the callback as 1st argument. False to disable log. Default is 'error_log'. * } */ - public function __construct( TableDefinitionInterface $table, array $args = [] ) { + public function __construct( Table $table, array $args = [] ) { $this->table = $table; $this->handle_downgrade = ! empty( $args['handle_downgrade'] ); $this->upgrade_hook = isset( $args['upgrade_hook'] ) ? $args['upgrade_hook'] : 'admin_menu'; $this->upgrade_hook_prio = isset( $args['upgrade_hook_prio'] ) ? (int) $args['upgrade_hook_prio'] : 8; + $this->logger = isset( $args['logger'] ) ? $args['logger'] : null; if ( ! $this->table_is_up_to_date() ) { /** * The option doesn't exist or is not up-to-date: we must upgrade the table before declaring it ready. - * See self::maybe_upgrade_table() for the upgrade. + * See $this->maybe_upgrade_table() for the upgrade. */ return; } @@ -117,17 +136,6 @@ public function init() { add_action( $this->upgrade_hook, [ $this, 'maybe_upgrade_table' ], $this->upgrade_hook_prio ); } - /** - * Tell if the table is ready to be used. - * - * @since 0.1 - * - * @return bool - */ - public function table_is_ready(): bool { - return $this->table_ready; - } - /** ----------------------------------------------------------------------------------------- */ /** TABLE VERSION =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ @@ -147,10 +155,10 @@ public function table_is_up_to_date(): bool { } if ( $this->handle_downgrade ) { - return $table_version !== $this->table->get_table_version(); + return $table_version !== $this->table->get_table_definition()->get_table_version(); } - return $table_version >= $this->table->get_table_version(); + return $table_version >= $this->table->get_table_definition()->get_table_version(); } /** @@ -161,7 +169,7 @@ public function table_is_up_to_date(): bool { * @return int The version. 0 if not set yet. */ public function get_db_version(): int { - if ( $this->table->is_table_global() && is_multisite() ) { + if ( $this->table->get_table_definition()->is_table_global() && is_multisite() ) { return (int) get_site_option( $this->get_db_version_option_name() ); } @@ -176,10 +184,12 @@ public function get_db_version(): int { * @return void */ protected function update_db_version() { - if ( $this->table->is_table_global() && is_multisite() ) { - update_site_option( $this->get_db_version_option_name(), $this->table->get_table_version() ); + $table_definition = $this->table->get_table_definition(); + + if ( $table_definition->is_table_global() && is_multisite() ) { + update_site_option( $this->get_db_version_option_name(), $table_definition->get_table_version() ); } else { - update_option( $this->get_db_version_option_name(), $this->table->get_table_version() ); + update_option( $this->get_db_version_option_name(), $table_definition->get_table_version() ); } } @@ -191,7 +201,7 @@ protected function update_db_version() { * @return void */ protected function delete_db_version() { - if ( $this->table->is_table_global() && is_multisite() ) { + if ( $this->table->get_table_definition()->is_table_global() && is_multisite() ) { delete_site_option( $this->get_db_version_option_name() ); } else { delete_option( $this->get_db_version_option_name() ); @@ -206,7 +216,7 @@ protected function delete_db_version() { * @return string */ public function get_db_version_option_name(): string { - return $this->table->get_table_short_name() . self::TABLE_VERSION_OPTION_SUFFIX; + return $this->table->get_table_definition()->get_table_short_name() . self::TABLE_VERSION_OPTION_SUFFIX; } /** ----------------------------------------------------------------------------------------- */ @@ -229,10 +239,43 @@ public function maybe_upgrade_table() { return; } + if ( ! $this->table_is_allowed_to_upgrade() ) { + $this->set_table_not_ready(); + return; + } + // Create/Upgrade the table. $this->upgrade_table(); } + /** + * Tell if the table is allowed to be created/upgraded. + * + * @since 0.2 + * + * @return bool + */ + public function table_is_allowed_to_upgrade(): bool { + $allowed = true; + $table_version = $this->get_db_version(); + $table_definition = $this->table->get_table_definition(); + + if ( ! empty( $table_version ) && $table_definition->is_table_global() && ! wp_should_upgrade_global_tables() ) { + // The table exists, is global, but upgrade of the global tables is forbidden. + $allowed = false; + } + + /** + * Tell if the table is allowed to be created/upgraded. + * + * @since 0.2 + * + * @param bool $allowed True when the table is allowed to be created/upgraded. False otherwise. + * @param TableDefinitionInterface $table_definition An instance of the TableDefinitionInterface used. + */ + return (bool) apply_filters( 'screenfeed_autowpdb_table_is_allowed_to_upgrade', $allowed, $table_definition ); + } + /** * Create/Upgrade the table in the database. * @@ -241,10 +284,15 @@ public function maybe_upgrade_table() { * @return void */ public function upgrade_table() { - if ( ! DBUtilities::create_table( $this->table->get_table_name(), $this->table->get_table_schema() ) ) { + $upgraded = $this->table->create( + [ + 'logger' => $this->logger, + ] + ); + + if ( ! $upgraded ) { // Failure. $this->set_table_not_ready(); - $this->delete_db_version(); return; } @@ -253,6 +301,47 @@ public function upgrade_table() { $this->update_db_version(); } + /** ----------------------------------------------------------------------------------------- */ + /** TABLE DELETION ========================================================================== */ + /** ----------------------------------------------------------------------------------------- */ + + /** + * Delete the table from the database. + * + * @since 0.2 + * + * @return void + */ + public function delete_table() { + $deleted = $this->table->delete( + [ + 'logger' => $this->logger, + ] + ); + + if ( ! $deleted ) { + return; + } + + $this->set_table_not_ready(); + $this->delete_db_version(); + } + + /** ----------------------------------------------------------------------------------------- */ + /** TABLE READY ============================================================================= */ + /** ----------------------------------------------------------------------------------------- */ + + /** + * Tell if the table is ready to be used. + * + * @since 0.1 + * + * @return bool + */ + public function table_is_ready(): bool { + return $this->table_ready; + } + /** * Set various properties to tell the table is ready to be used. * @@ -263,11 +352,12 @@ public function upgrade_table() { protected function set_table_ready() { global $wpdb; - $table_short_name = $this->table->get_table_short_name(); + $table_definition = $this->table->get_table_definition(); + $table_short_name = $table_definition->get_table_short_name(); $this->table_ready = true; - $wpdb->$table_short_name = $this->table->get_table_name(); + $wpdb->$table_short_name = $table_definition->get_table_name(); - if ( $this->table->is_table_global() ) { + if ( $table_definition->is_table_global() ) { $wpdb->global_tables[] = $table_short_name; } else { $wpdb->tables[] = $table_short_name; @@ -284,11 +374,12 @@ protected function set_table_ready() { protected function set_table_not_ready() { global $wpdb; - $table_short_name = $this->table->get_table_short_name(); + $table_definition = $this->table->get_table_definition(); + $table_short_name = $table_definition->get_table_short_name(); $this->table_ready = false; unset( $wpdb->$table_short_name ); - if ( $this->table->is_table_global() ) { + if ( $table_definition->is_table_global() ) { $wpdb->global_tables = array_diff( $wpdb->global_tables, [ $table_short_name ] ); } else { $wpdb->tables = array_diff( $wpdb->tables, [ $table_short_name ] );