|
| 1 | +# WordPress Custom Table Manager |
| 2 | + |
| 3 | +A PHP library using PSR-3 logging to manage the lifecycle (installation, updates, deletion) of custom WordPress database tables via configuration. |
| 4 | + |
| 5 | +This library focuses on schema management (CREATE, ALTER, DROP) and does not handle data manipulation (INSERT, SELECT, UPDATE, DELETE). |
| 6 | + |
| 7 | +## Motivation |
| 8 | +I wanted to have a simple schema-manager, where all table configuration (with updates) is visible in a single file. |
| 9 | +The only dependency is PSR3 logger and WordPress. |
| 10 | + |
| 11 | +## Installation |
| 12 | + |
| 13 | +```bash |
| 14 | +composer require dol-lab/custom-table-manager psr/log |
| 15 | +``` |
| 16 | +*(Requires a PSR-3 logger implementation like `monolog/monolog` if logging is desired)* |
| 17 | + |
| 18 | +## Usage |
| 19 | + |
| 20 | +### 1. Define Table Configurations |
| 21 | + |
| 22 | +Create a configuration file (e.g., `config/database-tables.php`) that returns an array defining your tables. |
| 23 | + |
| 24 | +**Important:** Your plugin code is responsible for determining the correct, fully prefixed table names (using `$wpdb->prefix`) and replacing any placeholders like `{name}` or `{columns_create}` in the `updates` SQL strings *before* passing the configuration to the `SchemaManager`. |
| 25 | + |
| 26 | +```php |
| 27 | +<?php |
| 28 | +/** |
| 29 | + * Database table definitions configuration. |
| 30 | + * |
| 31 | + * @package YourPlugin |
| 32 | + */ |
| 33 | + |
| 34 | +// Example of preparing names and update SQL *before* returning the array: |
| 35 | +// global $wpdb; |
| 36 | +// $logs_table_name = $wpdb->prefix . 'myplugin_logs'; |
| 37 | +// $items_table_name = $wpdb->prefix . 'myplugin_items'; |
| 38 | +// |
| 39 | +// $log_update_sql_1_1_0 = "ALTER TABLE `{$logs_table_name}` ADD COLUMN `log_context` varchar(255) NULL AFTER `log_message`"; |
| 40 | +// $item_update_sql_1_3_0 = "ALTER TABLE `{$items_table_name}` ADD COLUMN `item_status` VARCHAR(20) NOT NULL DEFAULT 'active' AFTER `item_name`"; |
| 41 | + |
| 42 | +return array( |
| 43 | + // Example Log Table |
| 44 | + array( |
| 45 | + 'name' => 'my_logs', |
| 46 | + 'columns' => array( |
| 47 | + 'log_id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| 48 | + 'log_type' => 'varchar(50) NOT NULL', |
| 49 | + 'log_message' => 'text', |
| 50 | + 'log_time' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP', |
| 51 | + ), |
| 52 | + // The library replaces {name} and {columns_create} in this template |
| 53 | + 'create' => "CREATE TABLE {name} ( |
| 54 | + {columns_create}, /* placeholder {columns_create} automatically generated from columns above. */ |
| 55 | + PRIMARY KEY (log_id), |
| 56 | + KEY log_type (log_type) |
| 57 | + )", |
| 58 | + // Updates: Use actual SQL or Closures. |
| 59 | + 'updates' => array( |
| 60 | + // Version where 'log_context' column was added |
| 61 | + '1.1.0' => 'create', // create is a special keyword. |
| 62 | + // Version with multiple changes (array of SQL strings) |
| 63 | + '1.2.0' => array( |
| 64 | + "ALTER TABLE {name} MODIFY COLUMN `log_message` LONGTEXT", |
| 65 | + "ALTER TABLE {name} ADD INDEX `log_time_idx` (`log_time`)" |
| 66 | + ), |
| 67 | + // Example using a Closure (receives $wpdb, $table_name) |
| 68 | + '1.4.0' => function(\wpdb $wpdb, string $table_name) { |
| 69 | + // Perform complex update logic here |
| 70 | + // $wpdb->query(...); |
| 71 | + } |
| 72 | + ), |
| 73 | + ), |
| 74 | + |
| 75 | + // Example Items Table (Introduced in version 1.1.0) |
| 76 | + array( |
| 77 | + 'name' => 'my_items', |
| 78 | + 'columns' => array( |
| 79 | + 'item_id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| 80 | + 'item_name' => 'varchar(255) NOT NULL', |
| 81 | + 'item_status' => 'varchar(255) NOT NULL', |
| 82 | + 'date_added' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP', |
| 83 | + ), |
| 84 | + 'create' => "CREATE TABLE {name} ( |
| 85 | + {columns_create}, |
| 86 | + PRIMARY KEY (item_id), |
| 87 | + KEY item_name (item_name) --add index for better search performance. |
| 88 | + )", |
| 89 | + 'updates' => array( |
| 90 | + // Special keyword: table was added in this version. |
| 91 | + '1.1.0' => 'create', |
| 92 | + // Version where 'item_status' column was added |
| 93 | + '1.3.0' => 'ALTER TABLE {name} ADD item_status varchar(255) NOT NULL', |
| 94 | + ), |
| 95 | + ), |
| 96 | + // ... add more table definitions |
| 97 | +); |
| 98 | + |
| 99 | +``` |
| 100 | + |
| 101 | +**Configuration Array Keys:** |
| 102 | + |
| 103 | +* **`name`**: (string) The **full** WordPress table name, including the database prefix (e.g., `$wpdb->prefix . 'my_table'`). **Required**. |
| 104 | +* **`columns`**: (array) Associative array `['column_name' => 'SQL Definition']`. **Required**. |
| 105 | +* **`create`**: (string|false) Template for the `CREATE TABLE` statement. Use `{name}` for the table name and `{columns_create}` for the column definitions. Set to `false` to prevent automatic creation. **Required**. |
| 106 | +* **`updates`**: (array) *Optional*. Associative array `['plugin_version' => mixed]`. |
| 107 | + * Key: Plugin version string where the change was introduced. |
| 108 | + * Value: |
| 109 | + * `'create'`: Special string if the table was introduced in this version. |
| 110 | + * (string) A single SQL `ALTER TABLE` statement. **You must replace any table name placeholders manually.** |
| 111 | + * (array) An array of SQL `ALTER TABLE` strings. **You must replace any table name placeholders manually.** |
| 112 | + * (`Closure`) A function receiving `(\wpdb $wpdb, string $table_name)` for complex updates. |
| 113 | + |
| 114 | +### 2. Configure Logging (Optional) |
| 115 | + |
| 116 | +Provide a PSR-3 compatible logger instance (like Monolog or a custom one) to the `SchemaManager`. If omitted, a `NullLogger` is used (no logs). |
| 117 | + |
| 118 | +```php |
| 119 | +<?php |
| 120 | +// Example: Using NullLogger (no logging) |
| 121 | +use Psr\Log\NullLogger; |
| 122 | +$logger = new NullLogger(); |
| 123 | + |
| 124 | +// Example: Using Monolog (assuming installed and configured) |
| 125 | +// use Monolog\Logger; |
| 126 | +// use Monolog\Handler\StreamHandler; |
| 127 | +// $logger = new Logger('myplugin_db'); |
| 128 | +// $logger->pushHandler(new StreamHandler(WP_CONTENT_DIR . '/logs/myplugin_db.log', Logger::WARNING)); |
| 129 | + |
| 130 | +// Pass $logger when creating the SchemaManager instance. |
| 131 | +// $db_manager = new SchemaManager( $table_configs, $wpdb, $logger ); |
| 132 | +``` |
| 133 | + |
| 134 | +### 3. Integrate with Your Plugin |
| 135 | + |
| 136 | +Instantiate the `SchemaManager` and use its methods within your plugin's activation, update, and deactivation/uninstall hooks, using `try...catch` blocks for error handling. |
| 137 | + |
| 138 | +**Example Integration:** |
| 139 | + |
| 140 | +```php |
| 141 | +<?php |
| 142 | +/** |
| 143 | + * Plugin Name: My Custom Plugin |
| 144 | + * Description: Uses Custom Table Manager. |
| 145 | + * Version: 1.2.3 |
| 146 | + */ |
| 147 | + |
| 148 | +use DolLab\CustomTableManager\SchemaManager; |
| 149 | +use DolLab\CustomTableManager\TableOperationException; |
| 150 | +use DolLab\CustomTableManager\TableConfigurationException; |
| 151 | +use Psr\Log\LoggerInterface; |
| 152 | +use Psr\Log\NullLogger; // Or your chosen logger |
| 153 | + |
| 154 | +// Define constants for versions and options |
| 155 | +define('MYPLUGIN_VERSION', '1.2.3'); // Your current plugin version |
| 156 | +define('MYPLUGIN_DB_VERSION_OPTION', 'myplugin_db_version'); |
| 157 | + |
| 158 | +/** |
| 159 | + * Get the PSR-3 Logger instance. |
| 160 | + */ |
| 161 | +function myplugin_get_logger(): LoggerInterface { |
| 162 | + // Replace with your actual logger setup or NullLogger |
| 163 | + return new NullLogger(); |
| 164 | +} |
| 165 | + |
| 166 | +/** |
| 167 | + * Plugin Activation Hook. |
| 168 | + */ |
| 169 | +function myplugin_activate() { |
| 170 | + global $wpdb; |
| 171 | + $logger = myplugin_get_logger(); |
| 172 | + |
| 173 | + if (!class_exists(SchemaManager::class)) { |
| 174 | + $logger->critical('Custom Table Manager class not found. Run composer install.'); |
| 175 | + // Consider wp_die or admin notice |
| 176 | + return; |
| 177 | + } |
| 178 | + |
| 179 | + try { |
| 180 | + $table_configs = require plugin_dir_path(__FILE__) . 'config/database-tables.php'; |
| 181 | + if (empty($table_configs)) { |
| 182 | + $logger->error('Activation: No valid table configurations found after processing.'); |
| 183 | + return; // Or handle error appropriately |
| 184 | + } |
| 185 | + |
| 186 | + $db_manager = new SchemaManager($table_configs, $wpdb, $logger); |
| 187 | + $db_manager->install(); // Returns void on success, throws TableOperationException on failure |
| 188 | + |
| 189 | + // If install() didn't throw, it succeeded. |
| 190 | + update_option(MYPLUGIN_DB_VERSION_OPTION, MYPLUGIN_VERSION); |
| 191 | + $logger->info('Activation: Database tables installed/verified successfully.'); |
| 192 | + |
| 193 | + } catch (TableConfigurationException $e) { |
| 194 | + $logger->error('Activation Error: Invalid table configuration. ' . $e->getMessage()); |
| 195 | + // Add admin notice |
| 196 | + } catch (TableOperationException $e) { |
| 197 | + $logger->error('Activation Error: Failed to install/verify tables. ' . $e->getMessage()); |
| 198 | + // Add admin notice |
| 199 | + } catch (\Exception $e) { |
| 200 | + $logger->critical('Activation Error: An unexpected error occurred. ' . $e->getMessage()); |
| 201 | + // Add admin notice |
| 202 | + } |
| 203 | +} |
| 204 | +register_activation_hook(__FILE__, 'myplugin_activate'); |
| 205 | + |
| 206 | +/** |
| 207 | + * Check for database updates (e.g., on 'plugins_loaded'). |
| 208 | + */ |
| 209 | +function myplugin_check_for_updates() { |
| 210 | + global $wpdb; |
| 211 | + $logger = myplugin_get_logger(); |
| 212 | + $installed_version = get_option(MYPLUGIN_DB_VERSION_OPTION, '0.0.0'); |
| 213 | + |
| 214 | + if (version_compare($installed_version, MYPLUGIN_VERSION, '<')) { |
| 215 | + $logger->info("DB Update Check: Updating from v{$installed_version} to v" . MYPLUGIN_VERSION); |
| 216 | + |
| 217 | + if (!class_exists(SchemaManager::class)) { |
| 218 | + $logger->error('Update Check: Custom Table Manager class not found.'); |
| 219 | + return; // Add admin notice. |
| 220 | + } |
| 221 | + |
| 222 | + try { |
| 223 | + $table_configs = myplugin_get_processed_table_configs(); |
| 224 | + if (empty($table_configs)) { |
| 225 | + $logger->error('Update Check: No valid table configurations found after processing.'); |
| 226 | + return; // Or handle error |
| 227 | + } |
| 228 | + |
| 229 | + $db_manager = new SchemaManager($table_configs, $wpdb, $logger); |
| 230 | + // update_table_version returns void on success, throws TableOperationException on failure. |
| 231 | + $db_manager->update_table_version($installed_version, MYPLUGIN_VERSION); |
| 232 | + |
| 233 | + // If update_table_version() didn't throw, it succeeded. |
| 234 | + update_option(MYPLUGIN_DB_VERSION_OPTION, MYPLUGIN_VERSION); |
| 235 | + $logger->info("Update Check: Database update process completed successfully to v" . MYPLUGIN_VERSION); |
| 236 | + |
| 237 | + } catch (\Exception $e) { |
| 238 | + $logger->critical('Update Check Error: ' . $e->getMessage()); |
| 239 | + // Add admin notice. |
| 240 | + } |
| 241 | + } |
| 242 | +} |
| 243 | +add_action('plugins_loaded', 'myplugin_check_for_updates'); |
| 244 | + |
| 245 | +/** |
| 246 | + * Plugin Uninstall Hook (Optional). |
| 247 | + */ |
| 248 | +function myplugin_uninstall() { |
| 249 | + global $wpdb; |
| 250 | + // Option to preserve data |
| 251 | + // if (get_option('myplugin_preserve_data_on_uninstall')) { return; } |
| 252 | + |
| 253 | + $logger = myplugin_get_logger(); |
| 254 | + if (!class_exists(SchemaManager::class)) { |
| 255 | + $logger->warning('Uninstall: Custom Table Manager class not found. Tables may not be dropped.'); |
| 256 | + // Don't necessarily return, still try to delete options. |
| 257 | + } else { |
| 258 | + try { |
| 259 | + $table_configs = myplugin_get_processed_table_configs(); |
| 260 | + // Pass configs even if empty, uninstall should try based on names if possible. |
| 261 | + $db_manager = new SchemaManager($table_configs, $wpdb, $logger); |
| 262 | + $db_manager->uninstall(); // Returns void on success, throws TableOperationException on failure. |
| 263 | + $logger->info('Uninstall: Table drop process completed successfully (or tables did not exist).'); |
| 264 | + |
| 265 | + } catch (\Exception $e) { |
| 266 | + $logger->critical('Uninstall Error: ' . $e->getMessage()); |
| 267 | + } |
| 268 | + } |
| 269 | + |
| 270 | + // Delete options regardless of table drop success/failure |
| 271 | + // delete_option(MYPLUGIN_DB_VERSION_OPTION); |
| 272 | + // Delete other plugin options... |
| 273 | +} |
| 274 | +// register_uninstall_hook(__FILE__, 'myplugin_uninstall'); // Uncomment if needed |
| 275 | + |
| 276 | +``` |
| 277 | + |
| 278 | +### Manager Methods |
| 279 | + |
| 280 | +* **`install(): void`**: Creates tables defined in the configuration if they don't exist. Throws `TableOperationException` on failure. |
| 281 | +* **`update_table_version(string $old_plugin_version, string $new_plugin_version): void`**: Applies `updates` from the configuration based on version changes. Throws `TableOperationException` on failure. |
| 282 | +* **`uninstall(): void`**: Drops all tables defined in the configuration. Throws `TableOperationException` on failure. |
| 283 | + |
| 284 | +Use `try...catch` blocks to handle potential `TableConfigurationException` (during instantiation) and `TableOperationException` (during operations). |
| 285 | + |
| 286 | +### Important Notes |
| 287 | + |
| 288 | +* **Error Handling**: Use `try...catch` blocks. Check logs via the injected PSR-3 logger for details. |
| 289 | +* **Direct Queries**: This library uses direct `CREATE TABLE` and `ALTER TABLE` queries, not WordPress's `dbDelta`. Ensure your SQL syntax is correct for your target MySQL/MariaDB versions. |
| 290 | +* **Versioning**: Use semantic versioning. Store the installed *database schema* version (usually the plugin version when the schema was last successfully updated) in `wp_options` to manage updates correctly. |
0 commit comments