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