Skip to main content

Addon Lifecycle

This guide covers how addons are installed, updated, activated, deactivated, and removed from Mumara Campaigns.

Lifecycle States​

Addons go through various states during their lifecycle:

Available → Installing → Installed/Active → Updating → Active
↓
Deactivating → Inactive → Uninstalling → Removed
StateDescription
AvailableAddon files exist, not installed
InstallingInstallation in progress
ActiveInstalled and enabled
InactiveInstalled but disabled
UpdatingUpdate in progress
UninstallingRemoval in progress
RemovedFiles deleted

Addon Tracking​

Addon states are tracked in two places:

  1. Database - addons table stores metadata
  2. File - /storage/addons_statuses.json tracks enabled/disabled

Database Schema​

CREATE TABLE `addons` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(100) NOT NULL,
`type` VARCHAR(50) DEFAULT NULL,
`vendor` VARCHAR(100) DEFAULT NULL,
`installed_version` VARCHAR(20) DEFAULT NULL,
`available_version` VARCHAR(20) DEFAULT NULL,
`status` ENUM('available', 'installed', 'active', 'inactive') DEFAULT 'available',
`error` TEXT DEFAULT NULL,
`install_dir` VARCHAR(100) DEFAULT NULL,
`license_key` VARCHAR(255) DEFAULT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

Managing Addons from the Command Line​

Besides the admin panel, addons can be managed entirely from the CLI. These Artisan commands run the same logic as the admin panel buttons — they update the addons table, toggle the laravel-modules state, and refresh the framework caches — so the two paths never drift.

note

In the examples below, replace EmailValidator with your addon's install-dir name — the folder name under Addons/ (also the install_dir value in the addons table).

CommandWhat it does
php artisan addon:install <name>Runs the addon's install_addon() setup, creates/updates the addons record, enables the module, runs migrations, publishes config/translations, creates the public asset symlink, and refreshes caches
php artisan addon:uninstall <name>Rolls back the addon's migrations, removes the public asset symlink, disables the module, sets the record back to available, and refreshes caches
php artisan addon:enable <name>Marks an installed addon active, enables the module, and refreshes caches
php artisan addon:disable <name>Marks an installed addon inactive, disables the module, and refreshes caches
# Install (or re-install) an addon
php artisan addon:install EmailValidator

# Temporarily turn an installed addon off / back on
php artisan addon:disable EmailValidator
php artisan addon:enable EmailValidator

# Uninstall (rolls back migrations; addon files are kept)
php artisan addon:uninstall EmailValidator

addon:enable, addon:disable, and addon:uninstall require the addon to be installed first (they look it up in the addons table) and report "not found" otherwise — the same behavior as the admin panel.

Prefer addon:* over module:enable/module:disable

laravel-modules ships module:enable / module:disable, but those only flip the enabled flag in storage/addons_statuses.json. They do not update the addons table or clear caches, which can leave the database advertising an addon as active while its routes and service providers are not registered — surfacing as a Route ... not defined error. Always use the addon:* commands for installed addons.

Caches are refreshed automatically​

Installing, uninstalling, enabling, or disabling an addon changes which service providers and routes are registered. Each addon:* command clears the route, config, and view caches and drops the compiled module manifest (bootstrap/cache/modules.php) so the change takes effect on the next request — no manual php artisan optimize:clear required.

On production, framework caches are often pre-compiled with php artisan optimize (a cached route table in particular will hide a newly installed addon's routes). The commands leave the caches cleared afterwards; re-run php artisan optimize on your next deploy if you want the optimized state restored.

Composer dependencies​

The addon:* commands never run Composer. If an addon declares new third-party packages in its composer.json, install them once at the project root before enabling the addon — otherwise its classes will fail to load:

composer install --no-dev --optimize-autoloader
php artisan addon:install EmailValidator

Installation Process​

When an addon is installed:

1. Pre-Installation Checks​

// Check dependencies
$missing = check_addon_dependencies('EmailValidator');
if (!empty($missing)) {
throw new \Exception('Missing dependencies: ' . implode(', ', $missing));
}

// Check license (if applicable)
if ($addon->license_type !== 'free') {
$valid = AddonLicense::verify($addon->license_key, $addon->name);
if (!$valid) {
throw new \Exception('Invalid license key');
}
}

2. Database Setup​

// Run migrations
Artisan::call('module:migrate', ['module' => 'EmailValidator']);

// Execute install SQL files
$installDir = addonInstallDir('EmailValidator');
foreach (glob($installDir . '/*.sql') as $sqlFile) {
$sql = file_get_contents($sqlFile);
DB::unprepared($sql);
}

3. Configuration Publishing​

// Publish config
Artisan::call('module:publish-config', ['module' => 'EmailValidator']);

// Publish translations
Artisan::call('module:publish-translation', ['module' => 'EmailValidator']);

A symlink is created so the addon's static assets (CSS, JS, images) are accessible from the browser:

// Create symlink: public/Addons/EmailValidator → Addons/EmailValidator/public
$addonPublicDir = base_path('Addons/EmailValidator/public');
$publicAddonsPath = public_path('Addons');
$symlinkTarget = $publicAddonsPath . '/EmailValidator';

if (is_dir($addonPublicDir) && !file_exists($symlinkTarget)) {
// Ensure parent directory exists
if (!is_dir($publicAddonsPath)) {
mkdir($publicAddonsPath, 0755, true);
}

symlink($addonPublicDir, $symlinkTarget);
}

This maps Addons/EmailValidator/public/css/addon.css to the URL /Addons/EmailValidator/css/addon.css.

5. Enable Addon​

// Enable in Laravel Modules
Artisan::call('module:enable', ['module' => 'EmailValidator']);

// Update database record
Addon::where('install_dir', 'EmailValidator')->update([
'status' => 'active',
'installed_version' => $addon->version,
]);

6. Clear Caches​

Artisan::call('cache:clear');
Artisan::call('config:clear');
Artisan::call('view:clear');
Artisan::call('route:clear');

Install Function​

Implement the install_addon function in your addon's functions.php. The function takes no parameters and must return an array:

// functions.php
<?php

/**
* Called during addon installation.
* The system calls install_addon() with no arguments.
*
* @return array Must contain 'success' key. Return ['success' => false, 'message' => '...'] to abort.
*/
function install_addon(): array
{
try {
// Optional: check dependencies
$checkDep = emailvalidator_checkDependency();
if ($checkDep !== 'success') {
return ['success' => false, 'message' => $checkDep];
}

// Optional: check license
$checkLic = emailvalidator_checkLicense();
if ($checkLic !== 'success') {
return ['success' => false, 'message' => $checkLic];
}

// Any custom setup logic (seed data, create directories, etc.)
// Note: migrations and module enabling are handled by the system automatically

return ['success' => true, 'message' => 'Installed successfully'];

} catch (\Exception $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}

/**
* Check addon dependencies.
* Return "success" or an error message string.
*/
function emailvalidator_checkDependency($request = null): string
{
// Check PHP extensions, external services, etc.
return "success";
}

/**
* Check addon license.
* Return "success" or an error message string.
*/
function emailvalidator_checkLicense(): mixed
{
return "success";
}
note

After install_addon() returns success, the system automatically:

  1. Creates/updates the addon record in the addons table
  2. Enables the module (module:enable)
  3. Runs database migrations
  4. Publishes translations and config
  5. Creates the public asset symlink

Update Process​

When an addon is updated:

1. Download Update​

// Get update from configured URL
$updateUrl = $addon->update_url;
$zipPath = storage_path('app/temp/addon-update.zip');

file_put_contents($zipPath, file_get_contents($updateUrl));

2. Backup Current Version​

$addonPath = addonDir('EmailValidator');
$backupPath = storage_path('app/backups/EmailValidator-' . $currentVersion);

// Create backup
File::copyDirectory($addonPath, $backupPath);

3. Extract and Replace​

$zip = new ZipArchive();
$zip->open($zipPath);
$zip->extractTo($addonPath);
$zip->close();

4. Run Update Scripts​

// Run new migrations
Artisan::call('module:migrate', ['module' => 'EmailValidator']);

// Run update SQL (only files newer than current version)
$installDir = addonInstallDir('EmailValidator');
foreach (glob($installDir . '/v*.sql') as $file) {
preg_match('/v([\d.]+)\.sql$/', $file, $matches);
$fileVersion = $matches[1] ?? '0';

if (version_compare($fileVersion, $currentVersion, '>')) {
$sql = file_get_contents($file);
DB::unprepared($sql);
}
}

5. Update Database Record​

Addon::where('install_dir', 'EmailValidator')->update([
'installed_version' => $newVersion,
'available_version' => $newVersion,
]);

Update Function​

The update_addon function takes no parameters and must return an array. On success, it should include the update_url where the system can download the new version:

// functions.php

/**
* Called before updating the addon.
* The system calls update_addon() with no arguments.
*
* @return array On success: ['success' => true, 'update_url' => '...']. On failure: ['success' => false, 'message' => '...']
*/
function update_addon(): array
{
try {
// Optional: verify license and dependencies before allowing update
$checkLic = emailvalidator_checkLicense();
$checkDep = emailvalidator_checkDependency();

if ($checkDep !== 'success' || $checkLic !== 'success') {
return ['success' => false, 'message' => $checkDep !== 'success' ? $checkDep : $checkLic];
}

// Return the URL where the system can download the update package
return [
'success' => true,
'update_url' => 'https://yoursite.com/addons/emailvalidator/update.zip',
];

} catch (\Exception $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
note

After update_addon() returns success, the system downloads the update package from update_url, extracts it, runs new migrations, and updates the version record.

Activation/Deactivation​

Activate Addon​

// Enable module
Artisan::call('module:enable', ['module' => 'EmailValidator']);

// Update status
Addon::where('install_dir', 'EmailValidator')->update([
'status' => 'active',
]);
note

The symlink is created during installation, not activation. Activate/deactivate only toggles the module's enabled state.

Deactivate Addon​

// Disable module
Artisan::call('module:disable', ['module' => 'EmailValidator']);

// Update status
Addon::where('install_dir', 'EmailValidator')->update([
'status' => 'inactive',
]);

Uninstallation​

Uninstall (Keep Files)​

// Rollback migrations
Artisan::call('migrate:rollback', [
'--path' => 'Addons/EmailValidator/Database/Migrations',
'--step' => 9999,
'--force' => true,
]);

// Disable module
Artisan::call('module:disable', ['module' => 'EmailValidator']);

// Remove public asset symlink
$symlinkPath = public_path('Addons/EmailValidator');
if (is_link($symlinkPath)) {
unlink($symlinkPath);
}

// Update status
Addon::where('install_dir', 'EmailValidator')->update([
'status' => 'available',
]);

Remove (Delete Files)​

When an addon is removed via the admin panel, the system:

  1. Rolls back all migrations
  2. Disables the module
  3. Removes the public asset symlink
  4. Deletes the addon database record
  5. Deletes the addon directory from Addons/

Uninstall Behavior​

The system handles uninstallation automatically -- there is no uninstall_addon() function to implement. When the admin uninstalls your addon, the system:

  1. Rolls back all addon migrations (migrate:rollback)
  2. Removes the public asset symlink (public/Addons/YourAddonName)
  3. Disables the module (module:disable)
  4. Updates the addon status to available

If you need cleanup beyond migration rollback (e.g., removing files from storage), use the Settings/uninstall/uninstall.sql script for database cleanup.

SQL Scripts​

Install SQL (Settings/install/v1.0.sql)​

-- Version 1.0 Installation

CREATE TABLE IF NOT EXISTS `email_validator_results` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`user_id` INT UNSIGNED NOT NULL,
`email` VARCHAR(320) NOT NULL,
`status` ENUM('valid', 'invalid', 'unknown', 'pending') DEFAULT 'pending',
`score` TINYINT UNSIGNED DEFAULT 0,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_user_id` (`user_id`),
INDEX `idx_email` (`email`),
INDEX `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS `email_validator_settings` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`key` VARCHAR(100) NOT NULL,
`value` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `key_unique` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Update SQL (Settings/install/v1.1.sql)​

-- Version 1.1 Updates

ALTER TABLE `email_validator_results`
ADD COLUMN IF NOT EXISTS `mx_found` TINYINT(1) DEFAULT 0 AFTER `score`,
ADD COLUMN IF NOT EXISTS `is_disposable` TINYINT(1) DEFAULT 0 AFTER `mx_found`;

CREATE TABLE IF NOT EXISTS `email_validator_disposable_domains` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`domain` VARCHAR(255) NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `domain_unique` (`domain`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Uninstall SQL (Settings/uninstall/uninstall.sql)​

-- Uninstall Script

DROP TABLE IF EXISTS `email_validator_results`;
DROP TABLE IF EXISTS `email_validator_settings`;
DROP TABLE IF EXISTS `email_validator_disposable_domains`;
DROP TABLE IF EXISTS `email_validator_batches`;

-- Remove columns added to users table
ALTER TABLE `users`
DROP COLUMN IF EXISTS `email_validation_credits`,
DROP COLUMN IF EXISTS `total_validations`;

Version Checking​

Check for updates from remote server:

// Check for new version
$currentVersion = config('emailvalidator.version');
$checkUrl = config('emailvalidator.new_version_url');

$response = Http::get($checkUrl);
$data = $response->json();

if (version_compare($data['version'], $currentVersion, '>')) {
// Update available
Addon::where('install_dir', 'EmailValidator')->update([
'available_version' => $data['version'],
]);
}

Best Practices​

  1. Always backup - Create backups before updates
  2. Version your SQL - Use version numbers in filenames
  3. Test migrations - Ensure rollback works correctly
  4. Clean up on uninstall - Remove all addon data
  5. Fire hooks - Allow other addons to react to lifecycle events
  6. Log operations - Record installation/update/uninstall actions
  7. Handle errors gracefully - Provide meaningful error messages