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 ] );