# easy-invoice/2.3.1/includes/Addons/AddonManager.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.3.1. 173 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.3.1/code/includes/Addons/AddonManager.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.3.1/raw/includes/Addons/AddonManager.php
- Modified: 2026-05-21T08:29:24+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/easy-invoice/2.3.1/code/includes/Addons/AddonManager.php#L10-L20`.

```php
<?php
/**
 * AddonManager — single source of truth for "is this addon on?".
 *
 * Used by:
 *   • The Free plugin's Addons admin page (to render card state)
 *   • The Pro plugin's bootstrap (to decide which addon files to require)
 *
 * Guarantees:
 *   • Disabled addons contribute zero PHP execution.
 *   • A user without the matching license tier cannot enable an addon, even if
 *     the wp_option was forced on directly.
 *
 * @package EasyInvoice
 */

namespace EasyInvoice\Addons;

if (!defined('ABSPATH')) {
    exit;
}

class AddonManager {

    private const OPTION_PREFIX = 'easy_invoice_addon_';
    private const OPTION_SUFFIX = '_enabled';

    /**
     * Per-addon enable flag option key. Centralised so we don't drift.
     */
    public static function optionKey(string $addonId): string {
        return self::OPTION_PREFIX . $addonId . self::OPTION_SUFFIX;
    }

    /**
     * Has the site admin toggled this addon on?
     */
    public static function isEnabled(string $addonId): bool {
        return (bool) get_option(self::optionKey($addonId), 0);
    }

    /**
     * Does the user's license satisfy the addon's required plan?
     */
    public static function userCanAccess(string $addonId): bool {
        $addon = AddonRegistry::find($addonId);
        if (!$addon) {
            return false;
        }
        return LicensePlanResolver::planSatisfies($addon['plan']);
    }

    /**
     * Final gate: enabled AND license-eligible AND addon class loadable
     * via the composer autoloader. Every addon ships a `pro_class`
     * FQCN in its registry entry; the addons folder is mapped under
     * `EasyInvoicePro\Addons\` so dropping in `<Name>/<Name>.php` is
     * all it takes to make a new addon discoverable.
     */
    public static function shouldLoad(string $addonId): bool {
        if (!self::isEnabled($addonId))     return false;
        if (!self::userCanAccess($addonId)) return false;

        $addon = AddonRegistry::find($addonId);
        if (!$addon)                                return false;
        if (empty($addon['pro_class']))             return false;
        if (!defined('EASY_INVOICE_PRO_PLUGIN_DIR'))return false;

        // class_exists triggers the composer autoloader; if the class
        // file is missing or unmapped this returns false.
        return class_exists($addon['pro_class']);
    }

    /**
     * Enable an addon. Refuses if license tier is insufficient.
     *
     * @return array{success:bool, code:string, message:string, required_plan?:string}
     */
    public static function enable(string $addonId): array {
        $addon = AddonRegistry::find($addonId);
        if (!$addon) {
            return [
                'success' => false,
                'code'    => 'unknown_addon',
                'message' => __('That addon does not exist.', 'easy-invoice'),
            ];
        }

        if (!self::userCanAccess($addonId)) {
            return [
                'success'       => false,
                'code'          => 'upgrade_required',
                'message'       => sprintf(
                    /* translators: %s = required plan name */
                    __('This addon requires the %s plan.', 'easy-invoice'),
                    LicensePlanResolver::getPlanLabel($addon['plan'])
                ),
                'required_plan' => $addon['plan'],
            ];
        }

        update_option(self::optionKey($addonId), 1);
        do_action('easy_invoice_addon_enabled', $addonId, $addon);

        return [
            'success' => true,
            'code'    => 'enabled',
            'message' => __('Addon enabled.', 'easy-invoice'),
        ];
    }

    /**
     * Disable an addon. Always allowed — disabling is never blocked.
     *
     * @return array{success:bool, code:string, message:string}
     */
    public static function disable(string $addonId): array {
        $addon = AddonRegistry::find($addonId);
        if (!$addon) {
            return [
                'success' => false,
                'code'    => 'unknown_addon',
                'message' => __('That addon does not exist.', 'easy-invoice'),
            ];
        }

        update_option(self::optionKey($addonId), 0);
        do_action('easy_invoice_addon_disabled', $addonId, $addon);

        return [
            'success' => true,
            'code'    => 'disabled',
            'message' => __('Addon disabled.', 'easy-invoice'),
        ];
    }

    /**
     * Symbolic status for one addon — drives the card button state in the UI.
     *
     * @return string one of: 'active' | 'available' | 'locked'
     */
    public static function statusFor(string $addonId): string {
        if (!self::userCanAccess($addonId)) {
            return 'locked';
        }
        return self::isEnabled($addonId) ? 'active' : 'available';
    }

    /**
     * Pro plugin entrypoint: bootstrap every addon that passes
     * shouldLoad(). Drops a `<Name>/<Name>.php` into `addons/` with
     * the right namespace and PSR-4 + AddonManager take it from
     * there — no require_once, no addon.php, no composer.json change.
     *
     * Convention: the entry class exposes a static `bootstrap()` that
     * registers hooks / instantiates sub-controllers. Classes without
     * a bootstrap method are still considered "loaded" (the autoload
     * itself is enough — useful for marker-only addons).
     */
    public static function loadEnabledAddons(): void {
        foreach (AddonRegistry::all() as $addon) {
            if (!self::shouldLoad($addon['id'])) {
                continue;
            }
            $class = $addon['pro_class'];
            if (method_exists($class, 'bootstrap')) {
                call_user_func([$class, 'bootstrap']);
            }
            do_action('easy_invoice_addon_loaded', $addon['id'], $addon);
        }
    }
}

```
