Skip to content

Commit 15c2e07

Browse files
committed
feat: initial commit
0 parents  commit 15c2e07

File tree

12 files changed

+3767
-0
lines changed

12 files changed

+3767
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
vendor

README.md

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
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.

composer.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "dol-lab/custom-table-manager",
3+
"description": "A WordPress library to manage custom database table installation, updates, and deletion with configurable logging.",
4+
"type": "library",
5+
"license": "GPL-2.0-or-later",
6+
"authors": [
7+
{
8+
"name": "Vitus Schuhwerk",
9+
"email": "[email protected]"
10+
}
11+
],
12+
"require": {
13+
"php": ">=8.0",
14+
"psr/log": "^3.0"
15+
},
16+
"require-dev": {
17+
"phpunit/phpunit": "^9.6",
18+
"squizlabs/php_codesniffer": "^3.12",
19+
"wp-coding-standards/wpcs": "^3.1"
20+
},
21+
"autoload": {
22+
"psr-4": {
23+
"DolLab\\CustomTableManager\\": "src/"
24+
}
25+
},
26+
"autoload-dev": {
27+
"psr-4": {
28+
"DolLab\\CustomTableManager\\Tests\\": "tests/"
29+
}
30+
},
31+
"scripts": {
32+
"test": "phpunit"
33+
},
34+
"config": {
35+
"allow-plugins": {
36+
"dealerdirect/phpcodesniffer-composer-installer": true
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)