PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.2.0
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.2.0
1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
xspeed / includes / class-module-registry.php

class-module-registry.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.2.0, at includes/class-module-registry.php

231 lines 6.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Module registry — discovers, boots, and indexes all Modules (Free + Pro).
4 *
5 * Lifecycle:
6 * plugins_loaded(10) → Free modules register themselves via
7 * xspeed_register_modules action
8 * plugins_loaded(20) → Pro modules register (xspeed-pro hooks later)
9 * plugins_loaded(30) → Registry resolves dependencies, runs migrations,
10 * calls boot() on each available module in topo order
11 *
12 * No call site references concrete module classes directly. Inter-module
13 * lookups go through Module_Registry::get($slug). This is the architectural
14 * constraint that makes Pro↔Free portability cheap (see IMPLEMENTATION.md
15 * §1.3).
16 *
17 * @package XSpeed
18 */
19
20 namespace XSpeed;
21
22 defined( 'ABSPATH' ) || exit;
23
24 final class Module_Registry {
25
26 /**
27 * @var array<string,Module>
28 */
29 private static $modules = array();
30
31 /**
32 * @var bool
33 */
34 private static $booted = false;
35
36 /**
37 * Register a Module instance. Idempotent on slug — re-registration is
38 * a programming error and triggers a doing_it_wrong notice.
39 */
40 public static function register( Module $module ): void {
41 $slug = $module->slug();
42 if ( '' === $slug ) {
43 _doing_it_wrong( __METHOD__, esc_html__( 'Module is missing a SLUG constant.', 'xspeed' ), 'xspeed 1.1.0' );
44 return;
45 }
46 if ( isset( self::$modules[ $slug ] ) ) {
47 _doing_it_wrong( __METHOD__, esc_html( sprintf( 'Module "%s" already registered.', $slug ) ), 'xspeed 1.1.0' );
48 return;
49 }
50 self::$modules[ $slug ] = $module;
51 }
52
53 public static function get( string $slug ): ?Module {
54 return self::$modules[ $slug ] ?? null;
55 }
56
57 public static function has( string $slug ): bool {
58 return isset( self::$modules[ $slug ] );
59 }
60
61 /**
62 * Is the module registered AND available on this install (tier check)?
63 * Anything answering false is treated as "doesn't exist" by REST + UI.
64 */
65 public static function is_available( string $slug ): bool {
66 $m = self::get( $slug );
67 return $m && Tier_Registry::is_available( $m );
68 }
69
70 /**
71 * @return array<string,Module>
72 */
73 public static function all(): array {
74 return self::$modules;
75 }
76
77 /**
78 * @return array<string,Module>
79 */
80 public static function available(): array {
81 return array_filter(
82 self::$modules,
83 static function ( Module $m ) {
84 return Tier_Registry::is_available( $m );
85 }
86 );
87 }
88
89 /**
90 * @param string $tier Module::TIER_FREE or Module::TIER_PRO
91 * @return array<string,Module>
92 */
93 public static function list_by_tier( string $tier ): array {
94 return array_filter(
95 self::$modules,
96 static function ( Module $m ) use ( $tier ) {
97 return Tier_Registry::tier_of( $m ) === $tier;
98 }
99 );
100 }
101
102 /**
103 * Boot all available modules in dependency order. Called once during
104 * plugins_loaded. Calling twice is a no-op (guarded).
105 *
106 * Modules whose declared dependencies are missing or unavailable are
107 * SKIPPED with a debug-log notice — they never boot, REST callers see
108 * 404. We do not crash the site over a missing dep.
109 */
110 public static function boot_all(): void {
111 if ( self::$booted ) {
112 return;
113 }
114 self::$booted = true;
115
116 $available = self::available();
117 $ordered = self::topological_sort( $available );
118
119 foreach ( $ordered as $module ) {
120 $ok = true;
121 foreach ( $module->dependencies() as $dep_slug ) {
122 if ( ! self::is_available( $dep_slug ) ) {
123 self::log( sprintf( 'Module "%s" skipped: missing dependency "%s".', $module->slug(), $dep_slug ) );
124 $ok = false;
125 break;
126 }
127 }
128 if ( ! $ok ) {
129 continue;
130 }
131
132 // Run schema migrations for the stored version → current version.
133 Settings_Manager::run_migrations( $module );
134
135 $module->boot();
136
137 // Register REST routes + CLI commands declared by the module.
138 Rest_Manager::register_module( $module );
139 if ( defined( 'WP_CLI' ) && WP_CLI ) {
140 Cli_Manager::register_module( $module );
141 }
142 }
143 }
144
145 /**
146 * Activation hook propagator — calls activate() on every registered
147 * module. Free modules are registered before this runs (activation
148 * happens after the bootstrap require_onces). Pro modules run their
149 * own activation via the xspeed-pro main file.
150 */
151 public static function activate_all(): void {
152 foreach ( self::$modules as $module ) {
153 if ( Tier_Registry::is_available( $module ) ) {
154 $module->activate();
155 }
156 }
157 }
158
159 public static function deactivate_all(): void {
160 foreach ( self::$modules as $module ) {
161 if ( Tier_Registry::is_available( $module ) ) {
162 $module->deactivate();
163 }
164 }
165 }
166
167 /**
168 * Kahn's algorithm — deterministic topological sort. Modules with no
169 * remaining deps go first; cycles produce a doing_it_wrong notice and
170 * are returned in arbitrary order so the site still boots.
171 *
172 * @param array<string,Module> $modules
173 * @return Module[]
174 */
175 private static function topological_sort( array $modules ): array {
176 $in_degree = array();
177 $adj = array();
178
179 foreach ( $modules as $slug => $module ) {
180 $in_degree[ $slug ] = 0;
181 $adj[ $slug ] = array();
182 }
183 foreach ( $modules as $slug => $module ) {
184 foreach ( $module->dependencies() as $dep ) {
185 if ( ! isset( $modules[ $dep ] ) ) {
186 continue; // missing-dep modules are caught in boot_all().
187 }
188 $adj[ $dep ][] = $slug;
189 $in_degree[ $slug ] = ( $in_degree[ $slug ] ?? 0 ) + 1;
190 }
191 }
192
193 $queue = array();
194 foreach ( $in_degree as $slug => $deg ) {
195 if ( 0 === $deg ) {
196 $queue[] = $slug;
197 }
198 }
199
200 $ordered = array();
201 while ( $queue ) {
202 $slug = array_shift( $queue );
203 $ordered[] = $modules[ $slug ];
204 foreach ( $adj[ $slug ] as $next ) {
205 if ( --$in_degree[ $next ] === 0 ) {
206 $queue[] = $next;
207 }
208 }
209 }
210
211 if ( count( $ordered ) !== count( $modules ) ) {
212 _doing_it_wrong( __METHOD__, esc_html__( 'Circular module dependency detected; boot order is undefined for the offending modules.', 'xspeed' ), 'xspeed 1.1.0' );
213 // Append any modules left out (in cycle) so they still get a chance.
214 foreach ( $modules as $slug => $module ) {
215 if ( ! in_array( $module, $ordered, true ) ) {
216 $ordered[] = $module;
217 }
218 }
219 }
220
221 return $ordered;
222 }
223
224 private static function log( string $msg ): void {
225 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
226 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
227 error_log( '[xspeed] ' . $msg );
228 }
229 }
230 }
231