PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.0
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.0
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 1.2.0 All 28 releases
xspeed / includes / class-module-registry.php

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

233 lines 6.4 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. Plugin::activate() fills the registry first via
148 * Plugin::ensure_modules_registered(); plugins_loaded has already fired
149 * by the time activate_plugin() includes the plugin, so nothing else
150 * would. Pro modules run their own activation via the xspeed-pro main
151 * file.
152 */
153 public static function activate_all(): void {
154 foreach ( self::$modules as $module ) {
155 if ( Tier_Registry::is_available( $module ) ) {
156 $module->activate();
157 }
158 }
159 }
160
161 public static function deactivate_all(): void {
162 foreach ( self::$modules as $module ) {
163 if ( Tier_Registry::is_available( $module ) ) {
164 $module->deactivate();
165 }
166 }
167 }
168
169 /**
170 * Kahn's algorithm — deterministic topological sort. Modules with no
171 * remaining deps go first; cycles produce a doing_it_wrong notice and
172 * are returned in arbitrary order so the site still boots.
173 *
174 * @param array<string,Module> $modules
175 * @return Module[]
176 */
177 private static function topological_sort( array $modules ): array {
178 $in_degree = array();
179 $adj = array();
180
181 foreach ( $modules as $slug => $module ) {
182 $in_degree[ $slug ] = 0;
183 $adj[ $slug ] = array();
184 }
185 foreach ( $modules as $slug => $module ) {
186 foreach ( $module->dependencies() as $dep ) {
187 if ( ! isset( $modules[ $dep ] ) ) {
188 continue; // missing-dep modules are caught in boot_all().
189 }
190 $adj[ $dep ][] = $slug;
191 $in_degree[ $slug ] = ( $in_degree[ $slug ] ?? 0 ) + 1;
192 }
193 }
194
195 $queue = array();
196 foreach ( $in_degree as $slug => $deg ) {
197 if ( 0 === $deg ) {
198 $queue[] = $slug;
199 }
200 }
201
202 $ordered = array();
203 while ( $queue ) {
204 $slug = array_shift( $queue );
205 $ordered[] = $modules[ $slug ];
206 foreach ( $adj[ $slug ] as $next ) {
207 if ( --$in_degree[ $next ] === 0 ) {
208 $queue[] = $next;
209 }
210 }
211 }
212
213 if ( count( $ordered ) !== count( $modules ) ) {
214 _doing_it_wrong( __METHOD__, esc_html__( 'Circular module dependency detected; boot order is undefined for the offending modules.', 'xspeed' ), 'xspeed 1.1.0' );
215 // Append any modules left out (in cycle) so they still get a chance.
216 foreach ( $modules as $slug => $module ) {
217 if ( ! in_array( $module, $ordered, true ) ) {
218 $ordered[] = $module;
219 }
220 }
221 }
222
223 return $ordered;
224 }
225
226 private static function log( string $msg ): void {
227 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
228 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
229 error_log( '[xspeed] ' . $msg );
230 }
231 }
232 }
233