PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.3
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.3
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Addons / AddonManager.php

AddonManager.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.3.3, at includes/Addons/AddonManager.php

173 lines 5.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * AddonManager — single source of truth for "is this addon on?".
4 *
5 * Used by:
6 * • The Free plugin's Addons admin page (to render card state)
7 * • The Pro plugin's bootstrap (to decide which addon files to require)
8 *
9 * Guarantees:
10 * • Disabled addons contribute zero PHP execution.
11 * • A user without the matching license tier cannot enable an addon, even if
12 * the wp_option was forced on directly.
13 *
14 * @package EasyInvoice
15 */
16
17 namespace EasyInvoice\Addons;
18
19 if (!defined('ABSPATH')) {
20 exit;
21 }
22
23 class AddonManager {
24
25 private const OPTION_PREFIX = 'easy_invoice_addon_';
26 private const OPTION_SUFFIX = '_enabled';
27
28 /**
29 * Per-addon enable flag option key. Centralised so we don't drift.
30 */
31 public static function optionKey(string $addonId): string {
32 return self::OPTION_PREFIX . $addonId . self::OPTION_SUFFIX;
33 }
34
35 /**
36 * Has the site admin toggled this addon on?
37 */
38 public static function isEnabled(string $addonId): bool {
39 return (bool) get_option(self::optionKey($addonId), 0);
40 }
41
42 /**
43 * Does the user's license satisfy the addon's required plan?
44 */
45 public static function userCanAccess(string $addonId): bool {
46 $addon = AddonRegistry::find($addonId);
47 if (!$addon) {
48 return false;
49 }
50 return LicensePlanResolver::planSatisfies($addon['plan']);
51 }
52
53 /**
54 * Final gate: enabled AND license-eligible AND addon class loadable
55 * via the composer autoloader. Every addon ships a `pro_class`
56 * FQCN in its registry entry; the addons folder is mapped under
57 * `EasyInvoicePro\Addons\` so dropping in `<Name>/<Name>.php` is
58 * all it takes to make a new addon discoverable.
59 */
60 public static function shouldLoad(string $addonId): bool {
61 if (!self::isEnabled($addonId)) return false;
62 if (!self::userCanAccess($addonId)) return false;
63
64 $addon = AddonRegistry::find($addonId);
65 if (!$addon) return false;
66 if (empty($addon['pro_class'])) return false;
67 if (!defined('EASY_INVOICE_PRO_PLUGIN_DIR'))return false;
68
69 // class_exists triggers the composer autoloader; if the class
70 // file is missing or unmapped this returns false.
71 return class_exists($addon['pro_class']);
72 }
73
74 /**
75 * Enable an addon. Refuses if license tier is insufficient.
76 *
77 * @return array{success:bool, code:string, message:string, required_plan?:string}
78 */
79 public static function enable(string $addonId): array {
80 $addon = AddonRegistry::find($addonId);
81 if (!$addon) {
82 return [
83 'success' => false,
84 'code' => 'unknown_addon',
85 'message' => __('That addon does not exist.', 'easy-invoice'),
86 ];
87 }
88
89 if (!self::userCanAccess($addonId)) {
90 return [
91 'success' => false,
92 'code' => 'upgrade_required',
93 'message' => sprintf(
94 /* translators: %s = required plan name */
95 __('This addon requires the %s plan.', 'easy-invoice'),
96 LicensePlanResolver::getPlanLabel($addon['plan'])
97 ),
98 'required_plan' => $addon['plan'],
99 ];
100 }
101
102 update_option(self::optionKey($addonId), 1);
103 do_action('easy_invoice_addon_enabled', $addonId, $addon);
104
105 return [
106 'success' => true,
107 'code' => 'enabled',
108 'message' => __('Addon enabled.', 'easy-invoice'),
109 ];
110 }
111
112 /**
113 * Disable an addon. Always allowed — disabling is never blocked.
114 *
115 * @return array{success:bool, code:string, message:string}
116 */
117 public static function disable(string $addonId): array {
118 $addon = AddonRegistry::find($addonId);
119 if (!$addon) {
120 return [
121 'success' => false,
122 'code' => 'unknown_addon',
123 'message' => __('That addon does not exist.', 'easy-invoice'),
124 ];
125 }
126
127 update_option(self::optionKey($addonId), 0);
128 do_action('easy_invoice_addon_disabled', $addonId, $addon);
129
130 return [
131 'success' => true,
132 'code' => 'disabled',
133 'message' => __('Addon disabled.', 'easy-invoice'),
134 ];
135 }
136
137 /**
138 * Symbolic status for one addon — drives the card button state in the UI.
139 *
140 * @return string one of: 'active' | 'available' | 'locked'
141 */
142 public static function statusFor(string $addonId): string {
143 if (!self::userCanAccess($addonId)) {
144 return 'locked';
145 }
146 return self::isEnabled($addonId) ? 'active' : 'available';
147 }
148
149 /**
150 * Pro plugin entrypoint: bootstrap every addon that passes
151 * shouldLoad(). Drops a `<Name>/<Name>.php` into `addons/` with
152 * the right namespace and PSR-4 + AddonManager take it from
153 * there — no require_once, no addon.php, no composer.json change.
154 *
155 * Convention: the entry class exposes a static `bootstrap()` that
156 * registers hooks / instantiates sub-controllers. Classes without
157 * a bootstrap method are still considered "loaded" (the autoload
158 * itself is enough — useful for marker-only addons).
159 */
160 public static function loadEnabledAddons(): void {
161 foreach (AddonRegistry::all() as $addon) {
162 if (!self::shouldLoad($addon['id'])) {
163 continue;
164 }
165 $class = $addon['pro_class'];
166 if (method_exists($class, 'bootstrap')) {
167 call_user_func([$class, 'bootstrap']);
168 }
169 do_action('easy_invoice_addon_loaded', $addon['id'], $addon);
170 }
171 }
172 }
173