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
);

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