PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.8.0
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.8.0
3.8.0 3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 All 112 releases
templately / includes / Core / Module_Base.php

Module_Base.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! 3.8.0, at includes/Core/Module_Base.php

279 lines 9.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Module_Base — the contract every feature module extends.
4 *
5 * @package Templately
6 */
7
8 namespace Templately\Core;
9
10 /**
11 * Abstract base class for every auto-discovered feature module.
12 *
13 * A module lives in `modules/{kebab-name}/module.php` and declares a single class
14 * extending this base. The {@see Modules_Manager} discovers, orders, and instantiates
15 * these classes. The constructor is `final` so the boot lifecycle cannot be bypassed —
16 * override the lifecycle methods (`init_hooks()`, `register_rest_routes()`,
17 * `enqueue_assets()`) instead of the constructor.
18 *
19 * Two entry points with strictly separated responsibilities:
20 * - PHP (`module.php`, this class): WordPress hooks, REST endpoint registration, PHP
21 * asset enqueuing, and sub-component registration.
22 * - JS (`assets/js/index.ts`): SPA route registration via `addFilter('templately.routes')`
23 * and Redux reducer attachment. PHP `add_filter('templately.routes')` never reaches it —
24 * the two hook systems are independent.
25 *
26 * @see specs/004-core-module-infrastructure
27 */
28 abstract class Module_Base {
29 /**
30 * Named internal sub-components, keyed by id.
31 *
32 * @var array<string, mixed>
33 */
34 private $components = [];
35
36 /**
37 * Boot the module.
38 *
39 * Declared `final`: runs `init_hooks()` during construction and defers the REST /
40 * asset lifecycle to their proper hooks. `register_rest_routes()` is intentionally
41 * NOT called here — `register_rest_route()` only works on `rest_api_init`; calling it
42 * on `plugins_loaded` silently drops the route with a `_doing_it_wrong` notice
43 * (constitution §VIII). The manager wraps this constructor in a try/catch, so an
44 * exception thrown from `init_hooks()` removes the module from the active registry
45 * without aborting the boot of other modules (FR-010).
46 */
47 final public function __construct() {
48 $this->init_hooks();
49
50 add_action( 'rest_api_init', [ $this, 'boot_rest_routes' ] );
51 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
52 }
53
54 /**
55 * Unique kebab-case module name. Uniqueness is FR-003 (a duplicate is skipped);
56 * equality with the module's DIRECTORY name is the convention everything outside
57 * the manager keys on (JS asset discovery, the dependency gates, the autoloader
58 * fallback) — the manager boots a mismatched module but emits a notice.
59 *
60 * @return string
61 */
62 abstract public function get_name(): string;
63
64 /**
65 * Whether the module should boot.
66 *
67 * Evaluated by the manager BEFORE the real instantiation. Returning `false` skips the
68 * module entirely — no hooks, no REST endpoints, no assets (FR-002). MUST be safe to
69 * call on a not-yet-constructed instance (no dependence on constructor state).
70 *
71 * @return bool
72 */
73 public function is_active(): bool {
74 return true;
75 }
76
77 /**
78 * Whether a module HELD BACK from the current release may boot.
79 *
80 * Some modules are complete and tested but not part of the release being cut —
81 * they ship in the zip, dormant. Each of them answers `is_active()` with this, so
82 * the manager skips it entirely: no hooks, no REST routes, no assets, and the SPA
83 * pieces that key off what the module localizes never appear either.
84 *
85 * Off by default, everywhere. Turn them back on for development or QA with
86 * `define( 'TEMPLATELY_ENABLE_DEFERRED_MODULES', true );` in wp-config.php, or per
87 * module through the filter. The test bootstrap opts in so the dormant code keeps
88 * its coverage. Releasing a module means deleting its `is_active()` override, not
89 * flipping the default here.
90 *
91 * @param string $name The module's own name, for the filter.
92 * @return bool
93 */
94 public static function deferred_module_enabled( string $name ): bool {
95 $enabled = defined( 'TEMPLATELY_ENABLE_DEFERRED_MODULES' ) && TEMPLATELY_ENABLE_DEFERRED_MODULES;
96
97 /**
98 * Filters whether a module held back from the release may boot.
99 *
100 * @param bool $enabled The constant's answer (false when it is not defined).
101 * @param string $name The module name, e.g. 'mcp-server'.
102 */
103 return (bool) apply_filters( 'templately_deferred_module_enabled', $enabled, $name );
104 }
105
106 /**
107 * Module names this module depends on; drives topological boot ordering (FR-004..006).
108 *
109 * MUST be safe to call on a not-yet-constructed instance.
110 *
111 * @return string[]
112 */
113 public function get_dependencies(): array {
114 return [];
115 }
116
117 /**
118 * Declarative environment requirements the manager verifies BEFORE instantiation.
119 *
120 * Evaluated by the manager during discovery, AFTER {@see is_active()} passes and after
121 * the `templately_module_requirements_{name}` filter runs. A single unmet requirement
122 * drops the module entirely — no hooks, no REST endpoints, no assets — and records a
123 * human-readable reason in {@see Modules_Manager::get_unavailable_modules()}. Like
124 * `is_active()` and `get_dependencies()`, this MUST be safe to call on a
125 * not-yet-constructed instance (no dependence on constructor state).
126 *
127 * Every key is optional; an empty array (the default) imposes no requirement:
128 * - `php` (string) Minimum PHP version, compared with `version_compare( PHP_VERSION, … )`.
129 * - `wp` (string) Minimum WordPress version, compared against `get_bloginfo( 'version' )`.
130 * - `classes` (string[]) Every entry must satisfy `class_exists()` or `interface_exists()`.
131 * - `functions` (string[]) Every entry must satisfy `function_exists()`.
132 * - `plugins` (string[]) Active-plugin basenames (`'dir/file.php'`), checked against the
133 * active-plugins option (plus network-active plugins on multisite).
134 * - `multisite` (bool) Must equal `is_multisite()`.
135 *
136 * @return array
137 */
138 public function get_requirements(): array {
139 return [];
140 }
141
142 /**
143 * Experimental-flag metadata (FR-009). Metadata only — does not alter boot behaviour.
144 *
145 * @return array|false
146 */
147 public function get_experimental_data() {
148 return false;
149 }
150
151 /**
152 * Named sub-feature gates, each bound to a host capability key (spec 053).
153 *
154 * `[ 'gate-name' => 'capability-key' ]`. A gate NEVER drops the module —
155 * that is `get_requirements()`/`is_active()`'s job. A module with unmet
156 * gates boots normally; only the code paths it guards with
157 * {@see has_gate()} stay inactive. Unmet gates are surfaced per module in
158 * {@see Modules_Manager::get_unmet_gates()} for the developer console.
159 *
160 * @return array<string, string>
161 */
162 public function get_capability_gates(): array {
163 return [];
164 }
165
166 /**
167 * Whether one of this module's own declared gates is met (spec 053).
168 *
169 * Resolution is lazy — nothing is probed until asked — so this is safe at
170 * any point during or after `init_hooks()`. A gate name the module never
171 * declared answers false with a development-mode notice, never a fatal.
172 *
173 * @param string $gate Gate name declared in {@see get_capability_gates()}.
174 * @return bool
175 */
176 protected function has_gate( $gate ): bool {
177 $gates = $this->get_capability_gates();
178
179 if ( ! isset( $gates[ $gate ] ) ) {
180 _doing_it_wrong(
181 __METHOD__,
182 esc_html( sprintf( 'Templately module "%s" queried undeclared gate "%s"; answering unmet.', $this->get_name(), $gate ) ),
183 '3.8.0'
184 );
185 return false;
186 }
187
188 return Capabilities::get_instance()->has( $gates[ $gate ] );
189 }
190
191 /**
192 * Register WordPress hooks. The only lifecycle method that runs during construction.
193 *
194 * @return void
195 */
196 protected function init_hooks(): void {}
197
198 /**
199 * Register the module's REST endpoints.
200 *
201 * MUST call `->register_routes()` explicitly on every endpoint (never rely on a
202 * registry drain). Runs on `rest_api_init` via {@see boot_rest_routes()} — NEVER from
203 * the constructor (constitution §VIII).
204 *
205 * @return void
206 */
207 public function register_rest_routes(): void {}
208
209 /**
210 * The public `rest_api_init` callback the base constructor hooks. Kept separate from
211 * `register_rest_routes()` so the hook callback is stable and the intent is explicit.
212 *
213 * @return void
214 */
215 public function boot_rest_routes(): void {
216 $this->register_rest_routes();
217 }
218
219 /**
220 * Enqueue the module's PHP-side assets. Runs on `admin_enqueue_scripts`, not the
221 * constructor.
222 *
223 * @return void
224 */
225 public function enqueue_assets(): void {}
226
227 /**
228 * Register a named internal sub-component (FR-007).
229 *
230 * `@internal` — access is convention-bounded, not enforced by PHP visibility. A
231 * name collision preserves the first registrant and emits a diagnostic notice; it
232 * never overwrites (edge case: sub-component name collision).
233 *
234 * @param string $id Unique sub-component id within this module.
235 * @param mixed $instance The sub-component instance.
236 * @return void
237 */
238 public function add_component( string $id, $instance ): void {
239 if ( isset( $this->components[ $id ] ) ) {
240 $this->diagnostic_notice(
241 sprintf(
242 'Templately module "%s": sub-component "%s" is already registered; keeping the first instance.',
243 $this->get_name(),
244 $id
245 )
246 );
247 return;
248 }
249
250 $this->components[ $id ] = $instance;
251 }
252
253 /**
254 * Retrieve a named sub-component (FR-007).
255 *
256 * `@internal` — intended for use only from within the owning module. Returns `null`
257 * for an unregistered id — never a wrong instance.
258 *
259 * @param string $id Sub-component id.
260 * @return mixed|null
261 */
262 public function get_component( string $id ) {
263 return $this->components[ $id ] ?? null;
264 }
265
266 /**
267 * Emit a non-fatal diagnostic notice (FR-011).
268 *
269 * @param string $message Human-readable diagnostic.
270 * @return void
271 */
272 private function diagnostic_notice( string $message ): void {
273 trigger_error( esc_html( $message ), E_USER_NOTICE ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
274
275 // Templately's dedicated log file, not debug.log (WP_DEBUG_LOG-gated inside).
276 \Templately\Utils\Helper::log( $message, 'modules', 'warning' );
277 }
278 }
279