PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / trunk
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! vtrunk
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 / modules / utilities / Cleanup / TaskRegistry.php

TaskRegistry.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! trunk, at modules/utilities/Cleanup/TaskRegistry.php

271 lines 7.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The cleanup contribution registry.
4 *
5 * @package Templately
6 */
7
8 namespace Templately\Modules\Utilities\Cleanup;
9
10 use InvalidArgumentException;
11 use Templately\Utils\Helper;
12 use Throwable;
13
14 /**
15 * Features contribute cleanup tasks here; this class never names one.
16 *
17 * That inversion is the point. A direct call list would make this module depend
18 * on full-site-import, block-patterns, mcp-server and both developer modules —
19 * so the shared cleaner could not boot on a site where any of them is disabled,
20 * and every future feature producing disposable data would have to edit this
21 * file. The same reasoning, and the same shape, as `modules/mcp-core/`.
22 *
23 * Collection is LAZY and happens once per registry lifetime. Two reasons:
24 * - `descriptor()` translates its label, and a `__()` call during
25 * `plugins_loaded` (before the textdomain loads) returns the untranslated
26 * string forever;
27 * - a request that never touches cleanup should not construct seven task
28 * objects.
29 */
30 final class TaskRegistry {
31
32 /**
33 * Contribute cleanup tasks.
34 *
35 * Fired once, the first time the registry is used — a cron fire, a REST
36 * listing, a developer screen. Handlers receive the registry and call
37 * `register_classes()` on it.
38 */
39 const COLLECT_ACTION = 'templately_cleanup_register_tasks';
40
41 /** @var self|null */
42 private static $instance = null;
43
44 /** @var string[] Class names awaiting resolution. */
45 private $pending_classes = [];
46
47 /** @var array<string, TaskDescriptor> */
48 private $descriptors = [];
49
50 /** @var array<string, CleanupTask> */
51 private $tasks = [];
52
53 /** @var bool */
54 private $collected = false;
55
56 /** @var bool */
57 private $resolved = false;
58
59 public static function get_instance(): self {
60 if ( null === self::$instance ) {
61 self::$instance = new self();
62 }
63
64 return self::$instance;
65 }
66
67 /**
68 * Test seam. The next use re-fires {@see self::COLLECT_ACTION}, so the
69 * rebuilt registry is identical to the booted one — resetting must never
70 * leave a registry that only one module has repopulated.
71 */
72 public static function reset(): void {
73 self::$instance = null;
74 }
75
76 /**
77 * Queue task classes for lazy resolution.
78 *
79 * Takes CLASS NAMES, not instances: nothing is constructed and no
80 * `descriptor()` is called until the registry is actually used.
81 *
82 * @param string[] $classes
83 */
84 public function register_classes( array $classes ): void {
85 foreach ( $classes as $class ) {
86 $this->pending_classes[] = $class;
87 }
88
89 $this->resolved = false;
90 }
91
92 /**
93 * @return array<string, TaskDescriptor> Keyed by task id.
94 */
95 public function descriptors(): array {
96 $this->resolve();
97
98 return $this->descriptors;
99 }
100
101 /**
102 * Descriptors for tasks that may run unattended. Manual-only tasks are
103 * excluded from scheduled sweeps and from the run-everything action.
104 *
105 * @return array<string, TaskDescriptor>
106 */
107 public function schedulable_descriptors(): array {
108 return array_filter(
109 $this->descriptors(),
110 static function ( TaskDescriptor $descriptor ) {
111 return $descriptor->schedulable;
112 }
113 );
114 }
115
116 public function get_descriptor( string $id ): ?TaskDescriptor {
117 $descriptors = $this->descriptors();
118
119 return $descriptors[ $id ] ?? null;
120 }
121
122 /**
123 * @return string[]
124 */
125 public function schedulable_ids(): array {
126 return array_keys( $this->schedulable_descriptors() );
127 }
128
129 /**
130 * What one task would remove, without removing it.
131 */
132 public function estimate( string $id, Context $context ): TaskResult {
133 $task = $this->task( $id );
134 if ( null === $task ) {
135 return TaskResult::empty();
136 }
137
138 try {
139 return $task->estimate( $context->with_dry_run( true ) );
140 } catch ( Throwable $e ) {
141 return TaskResult::empty()->fail(
142 sprintf( 'Estimate failed for "%s": %s', $id, $e->getMessage() )
143 );
144 }
145 }
146
147 /**
148 * Run the named tasks in order, one result each.
149 *
150 * A task that throws is recorded and the sweep continues — one bad task must
151 * not cost the others their run. A task that exhausts the budget ends the
152 * sweep at that boundary; the rest are left for the next run.
153 *
154 * @param string[] $ids
155 * @return array<string, TaskResult>
156 */
157 public function run( array $ids, Context $context ): array {
158 $results = [];
159
160 foreach ( $ids as $id ) {
161 $task = $this->task( $id );
162 if ( null === $task ) {
163 continue;
164 }
165
166 if ( ! $context->has_budget() ) {
167 $results[ $id ] = TaskResult::empty()->stopped_on_budget();
168 break;
169 }
170
171 try {
172 $results[ $id ] = $task->run( $context );
173 } catch ( Throwable $e ) {
174 // Deliberately caught, not propagated: an unattended sweep that
175 // aborts on the first failing task leaves every later task
176 // permanently unrun, and the failure is invisible until someone
177 // reads the journal.
178 $results[ $id ] = TaskResult::empty()->fail(
179 sprintf( 'Cleanup task "%s" failed: %s', $id, $e->getMessage() )
180 );
181 Helper::log( sprintf( 'Cleanup task "%s" threw: %s', $id, $e->getMessage() ), 'cleanup', 'error' );
182 }
183 }
184
185 return $results;
186 }
187
188 /**
189 * Resolve a task instance, or null when the id is unknown.
190 */
191 private function task( string $id ): ?CleanupTask {
192 $this->resolve();
193
194 return $this->tasks[ $id ] ?? null;
195 }
196
197 /**
198 * Ask contributors to declare themselves, then turn class names into
199 * descriptors. Idempotent.
200 */
201 private function resolve(): void {
202 // Once per registry lifetime, and BEFORE the pending list is read below —
203 // handlers call register_classes(), which appends to it. `collected` is
204 // set first so a handler that touches the registry cannot recurse.
205 if ( ! $this->collected ) {
206 $this->collected = true;
207
208 /**
209 * Declare a module's cleanup tasks.
210 *
211 * @param TaskRegistry $registry Call `register_classes()` on it.
212 */
213 do_action( self::COLLECT_ACTION, $this );
214 }
215
216 if ( $this->resolved ) {
217 return;
218 }
219
220 // Set BEFORE the loop for the same reason.
221 $this->resolved = true;
222
223 $pending = $this->pending_classes;
224 $this->pending_classes = [];
225
226 foreach ( $pending as $class ) {
227 if ( ! is_string( $class ) || ! class_exists( $class ) ) {
228 // A module that was disabled or removed between registering and
229 // resolving simply contributes nothing. Not an error.
230 continue;
231 }
232
233 try {
234 $task = new $class();
235
236 if ( ! $task instanceof CleanupTask ) {
237 continue;
238 }
239
240 $descriptor = TaskDescriptor::from_array( $task->descriptor(), $class );
241 } catch ( InvalidArgumentException $e ) {
242 // A malformed descriptor is a bug in the contributing module.
243 // Skip that one task and keep the rest of the registry usable —
244 // taking the whole cleaner down over one bad contributor is
245 // worse than running without it.
246 Helper::log( $e->getMessage(), 'cleanup', 'error' );
247 continue;
248 } catch ( Throwable $e ) {
249 Helper::log(
250 sprintf( 'Cleanup task %s could not be constructed: %s', $class, $e->getMessage() ),
251 'cleanup',
252 'error'
253 );
254 continue;
255 }
256
257 if ( isset( $this->descriptors[ $descriptor->id ] ) ) {
258 Helper::log(
259 sprintf( 'Duplicate cleanup task id "%s" from %s — ignored.', $descriptor->id, $class ),
260 'cleanup',
261 'error'
262 );
263 continue;
264 }
265
266 $this->descriptors[ $descriptor->id ] = $descriptor;
267 $this->tasks[ $descriptor->id ] = $task;
268 }
269 }
270 }
271