Skip to content

Add plugin uninstall cleanup: remove tables, cron hooks, and options#48

Open
MathieuLamiot wants to merge 6 commits intodevelopfrom
feat/plugin-uninstall-cleanup
Open

Add plugin uninstall cleanup: remove tables, cron hooks, and options#48
MathieuLamiot wants to merge 6 commits intodevelopfrom
feat/plugin-uninstall-cleanup

Conversation

@MathieuLamiot
Copy link
Owner

Description

Fixes: #47

When a user deletes the Sybgo plugin from the WordPress admin, all plugin-created data is now removed: the four database tables, the four scheduled cron hooks, and the two WordPress options. This follows the WordPress plugin uninstall guidelines.

Type of change

  • New feature (non-breaking change which adds functionality).
  • Enhancement (non-breaking change which improves an existing functionality).

Detailed scenario

What was tested

Scenario 1 — Full uninstall on a live WordPress site

  1. Installed the plugin on a local WordPress site and activated it.
  2. Confirmed the four tables existed (wp_sybgo_events, wp_sybgo_reports, wp_sybgo_email_log, wp_sybgo_aggregated_events) via phpMyAdmin.
  3. Confirmed the two options existed (sybgo_settings, sybgo_email_recipients) via wp option get sybgo_settings.
  4. Confirmed four cron events were scheduled via wp cron event list | grep sybgo.
  5. Deleted the plugin via WP Admin → Plugins → Delete.
  6. Verified all four tables were gone via phpMyAdmin — none present.
  7. Verified both options were deleted: wp option get sybgo_settings returned "Error: Could not get 'sybgo_settings' option."
  8. Verified all four cron events were cleared: wp cron event list | grep sybgo returned no results.

Scenario 2 — Deactivation does not remove data

  1. Deactivated the plugin (not deleted).
  2. Verified tables and options still existed — confirmed data is preserved on deactivation.

How to test

  1. Install and activate the Sybgo plugin on a WordPress site.
  2. Verify the plugin tables exist:
    wp db query "SHOW TABLES LIKE 'wp_sybgo%'"
    
    Expected: 4 tables listed.
  3. Verify plugin options exist:
    wp option get sybgo_settings
    wp option get sybgo_email_recipients
    
    Expected: values returned for both.
  4. Verify cron events are scheduled:
    wp cron event list | grep sybgo
    
    Expected: 4 hooks listed.
  5. Go to WP Admin → Plugins, deactivate Sybgo, then click Delete.
  6. After deletion, re-run steps 2–4:
    • Step 2: no tables returned.
    • Step 3: both options return "Could not get option" error.
    • Step 4: no cron events returned.

Affected Features & Quality Assurance Scope

  • Plugin uninstall path (new)
  • Plugin deactivation (unchanged — cron clearing behaviour preserved)
  • Database tables (drop only on uninstall, not deactivation)
  • WordPress options (sybgo_settings, sybgo_email_recipients)

Technical description

Documentation

Updated: wp-plugin/docs/development.md — new "Plugin Uninstall" section explains the three-step cleanup and the single-source-of-truth pattern.

Implementation details

uninstall.php is the WordPress-recommended approach (vs register_uninstall_hook) because it runs in a clean context without loading the full plugin, avoiding the singleton constructor, DB setup, and action registrations.

Sybgo\Admin\Uninstaller::run() delegates to three focused methods:

  • drop_tables() — calls DatabaseManager::get_table_names() (static, no side effects, no table creation) and issues DROP TABLE IF EXISTS for each.
  • clear_cron_hooks() — calls Sybgo::get_cron_hooks() (new static method) and passes each to wp_clear_scheduled_hook().
  • delete_options() — calls Settings_Page::get_option_names() (new static method returning OPTION_NAME and LEGACY_OPTION_EMAIL_RECIPIENTS) and passes each to delete_option().

To avoid identifier duplication, three refactors were made:

  • DatabaseManager::get_table_names() converted to static — the constructor now uses it instead of repeating the prefix+name strings. The Factory call sites ($db_manager->get_table_names()) continue to work since PHP allows calling static methods on instances.
  • Sybgo::get_cron_hooks() added as public static; init_cron_schedules() and deactivate() refactored to call it.
  • Settings_Page::LEGACY_OPTION_EMAIL_RECIPIENTS constant added; activate() refactored to use it.

Unit tests in UninstallerTest cover all three methods and run(), using Brain\Monkey + Mockery. The MockeryPHPUnitIntegration trait is used to ensure Mockery expectations count as PHPUnit assertions.

New dependencies

None.

Risks

  • Data loss risk: The uninstall routine is intentionally destructive. This is the expected behaviour per WordPress guidelines. Deactivation is non-destructive, giving users a safe "pause" option before deciding to delete.
  • Static method refactor on DatabaseManager: Calling a static method via an instance ($obj->staticMethod()) is valid PHP but triggers a deprecation notice in strict tools. The existing Factory call sites do this — no behaviour change, but can be cleaned up in a follow-up.

Mandatory Checklist

Code validation

  • I validated all the Acceptance Criteria. If possible, provide screenshots or videos.
  • I triggered all changed lines of code at least once without new errors/warnings/notices.
  • I implemented built-in tests to cover the new/changed code.

Code style

  • I wrote a self-explanatory code about what it does.
  • I protected entry points against unexpected inputs.
  • I did not introduce unnecessary complexity.
  • Output messages (errors, notices, logs) are explicit enough for users to understand the issue and are actionable.

Unticked items justification

None.

Additional Checks

  • In the case of complex code, I wrote comments to explain it.
  • When possible, I prepared ways to observe the implemented system (logs, data, etc.).
  • I added error handling logic when using functions that could throw errors (HTTP/API request, filesystem, etc.)

Unticked additional checks justification

  • Observe the system: The uninstall routine removes all data by design; there is nothing to observe post-uninstall. The WordPress admin confirms the plugin is gone.
  • Error handling: $wpdb->query, wp_clear_scheduled_hook, and delete_option are WP core functions with their own error handling. The uninstall context does not support surfacing errors to the user, so wrapping them would add complexity with no benefit.

MathieuLamiot and others added 6 commits March 8, 2026 20:08
- Add `uninstall.php` entry point that runs `Uninstaller::run()` on plugin deletion
- Add `Uninstaller` class with focused methods: `drop_tables()`, `clear_cron_hooks()`, `delete_options()`
- Refactor `DatabaseManager::get_table_names()` to static (no-instantiation, no side effects)
- Add `Sybgo::get_cron_hooks()` static method; refactor `init_cron_schedules()` and `deactivate()` to use it
- Add `Settings_Page::LEGACY_OPTION_EMAIL_RECIPIENTS` constant and `get_option_names()` static method
- Add `UninstallerTest` covering drop_tables, clear_cron_hooks, delete_options, and run()
- Stub `register_activation_hook`, `register_deactivation_hook`, `add_action` in unit test bootstrap

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Uninstaller

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…o instance

- Constructor now only initializes table names (no side effects)
- New maybe_create_tables() method handles table creation + migration
- get_table_names() and drop_table() converted from static to instance methods
- Factory::create_database_manager() calls maybe_create_tables() explicitly
- Uninstaller uses a plain new DatabaseManager() instance (no table creation)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@MathieuLamiot MathieuLamiot self-assigned this Mar 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement uninstall cleanup: remove DB tables, cron jobs, and options

1 participant