Problem
The Package Manager uninstall dialog currently shows a fixed set of three radio buttons (preexisting_mode: Preserve / Uninstall / Restore). There is no way for a package to:
- Show a custom message before uninstall (e.g., "This package contains 1,523 orders and 450 products")
- Present custom options (e.g., checkboxes: "Remove database tables", "Delete media files", "Export data before removal")
- Collect user input (e.g., a "Reason for removal" textarea)
- Act on these choices during the uninstall process (in resolvers or events)
Complex packages like e-commerce systems, CRMs, or migration-heavy extras have no way to offer a controlled, user-guided uninstall experience. The user either gets a full uninstall with no options, or the package has to build an entirely separate uninstall page outside the standard workflow.
Current uninstall flow
User clicks "Uninstall" on package grid
→ MODx.window.PackageUninstall dialog
(3 radio buttons for preexisting_mode, nothing else)
→ User clicks "Uninstall"
→ AJAX: Workspace/Packages/Uninstall {signature, preexisting_mode}
→ Processor: $package->uninstall($options)
→ OnPackageUninstall event (too late, everything deleted)
No step in this chain allows the package to inject custom UI or custom parameters.
Proposed solution
1. Package declares uninstall setup
During package build, the developer registers an uninstall setup configuration — either as a static definition in transport attributes, or as a PHP file that returns a dynamic configuration.
Static approach (transport attributes during build):
$package->setAttribute('uninstall-setup', [
'message' => 'MiniShop3 contains important e-commerce data.',
'fields' => [
[
'xtype' => 'xcheckbox',
'name' => 'remove_tables',
'boxLabel' => 'Remove database tables (orders, products, categories)',
],
[
'xtype' => 'xcheckbox',
'name' => 'remove_media',
'boxLabel' => 'Remove uploaded product images',
],
],
]);
Dynamic approach (PHP file):
A file at a conventional path (e.g., {core_path}components/{package}/setup/uninstall-setup.php) returns the configuration at runtime. This allows dynamic content — counting records, checking disk usage, etc.:
<?php
// uninstall-setup.php — executed when the uninstall dialog opens
$orderCount = $modx->getCount('MiniShop3\Model\msOrder');
$productCount = $modx->getCount('MiniShop3\Model\msProductData');
return [
'message' => "MiniShop3 contains {$orderCount} orders and {$productCount} products.",
'fields' => [
[
'xtype' => 'xcheckbox',
'name' => 'remove_tables',
'boxLabel' => "Drop all database tables ({$orderCount} orders will be lost)",
],
[
'xtype' => 'xcheckbox',
'name' => 'remove_media',
'boxLabel' => 'Delete uploaded media files from filesystem',
],
[
'xtype' => 'textarea',
'name' => 'removal_reason',
'fieldLabel' => 'Reason for removal (optional)',
'anchor' => '100%',
],
],
];
2. Manager UI loads the setup
Before showing the uninstall dialog, the manager makes a request to a new processor (e.g., Workspace/Packages/GetUninstallSetup) which:
- Checks if the package has an
uninstall-setup attribute or a setup file
- Executes the setup file if present (or reads static attributes)
- Returns the form configuration to the client
The MODx.window.PackageUninstall dialog then renders the custom fields above the standard preexisting_mode radios. If no custom setup is defined, the dialog works exactly as before.
3. Custom options flow to server
When the user submits the dialog, all form values (standard + custom) are sent to the Uninstall processor. The processor includes them in the $options array:
// In Uninstall.php::process()
$options = [
xPDOTransport::PREEXISTING_MODE => $this->getProperty('preexisting_mode'),
// Custom options from the package's uninstall setup form:
'remove_tables' => $this->getProperty('remove_tables'),
'remove_media' => $this->getProperty('remove_media'),
'removal_reason' => $this->getProperty('removal_reason'),
];
4. Resolvers and events receive custom options
PHP resolvers during uninstall receive the full $options array and can act on the user's choices:
// In a package's PHP resolver:
if ($options[xPDOTransport::PACKAGE_ACTION] === xPDOTransport::ACTION_UNINSTALL) {
if (!empty($options['remove_tables'])) {
// Drop package tables
$modx->exec('DROP TABLE IF EXISTS ...');
}
if (!empty($options['remove_media'])) {
// Remove uploaded files
}
if (!empty($options['removal_reason'])) {
// Log or send analytics
}
}
The OnBeforePackageUninstall event (proposed in #16916) would also receive these options, enabling plugins to react to them.
Use cases
| Use case |
Custom field |
Resolver action |
| E-commerce: preserve orders |
Checkbox "Remove tables" |
Conditionally drop tables |
| Media-heavy package |
Checkbox "Delete uploaded files" |
Conditionally clean filesystem |
| SaaS integration |
Message "Disconnect from service first!" |
Informational, no action |
| Analytics / feedback |
Textarea "Reason for removal" |
Send to analytics endpoint |
| Migration system |
Checkbox "Rollback migrations" |
Run migration rollback |
Backwards compatibility
- Packages without
uninstall-setup — the dialog works exactly as before
- The standard
preexisting_mode radio buttons remain unchanged
- Existing resolvers continue to work — custom options are additive
- No changes to the xPDO transport system required
Related
Problem
The Package Manager uninstall dialog currently shows a fixed set of three radio buttons (
preexisting_mode: Preserve / Uninstall / Restore). There is no way for a package to:Complex packages like e-commerce systems, CRMs, or migration-heavy extras have no way to offer a controlled, user-guided uninstall experience. The user either gets a full uninstall with no options, or the package has to build an entirely separate uninstall page outside the standard workflow.
Current uninstall flow
No step in this chain allows the package to inject custom UI or custom parameters.
Proposed solution
1. Package declares uninstall setup
During package build, the developer registers an uninstall setup configuration — either as a static definition in transport attributes, or as a PHP file that returns a dynamic configuration.
Static approach (transport attributes during build):
Dynamic approach (PHP file):
A file at a conventional path (e.g.,
{core_path}components/{package}/setup/uninstall-setup.php) returns the configuration at runtime. This allows dynamic content — counting records, checking disk usage, etc.:2. Manager UI loads the setup
Before showing the uninstall dialog, the manager makes a request to a new processor (e.g.,
Workspace/Packages/GetUninstallSetup) which:uninstall-setupattribute or a setup fileThe
MODx.window.PackageUninstalldialog then renders the custom fields above the standardpreexisting_moderadios. If no custom setup is defined, the dialog works exactly as before.3. Custom options flow to server
When the user submits the dialog, all form values (standard + custom) are sent to the
Uninstallprocessor. The processor includes them in the$optionsarray:4. Resolvers and events receive custom options
PHP resolvers during uninstall receive the full
$optionsarray and can act on the user's choices:The
OnBeforePackageUninstallevent (proposed in #16916) would also receive these options, enabling plugins to react to them.Use cases
Backwards compatibility
uninstall-setup— the dialog works exactly as beforepreexisting_moderadio buttons remain unchangedRelated
OnBeforePackageUninstallevent (fires before uninstall while files still exist)