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 / full-site-import / Runners / Loop.php

Loop.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! trunk, at modules/full-site-import/Runners/Loop.php

353 lines 12.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Templately\Modules\FullSiteImport\Runners;
4
5 use Exception;
6 use Templately\Modules\FullSiteImport\Exception\FatalErrorException;
7 use Templately\Modules\FullSiteImport\Exception\SkippableErrorException;
8 use Templately\Modules\FullSiteImport\FullSiteImport;
9 use Templately\Modules\FullSiteImport\Concerns\LogHelper;
10 use Templately\Modules\FullSiteImport\Utils\SessionData;
11 use Templately\Modules\FullSiteImport\Utils\Utils;
12 use Templately\Utils\Helper;
13
14 /**
15 * @method string get_name()
16 * @method void sse_message(array $data)
17 */
18 trait Loop {
19
20 public static $max_error_attempts = 2;
21 public static $max_consecutive_skips = 5;
22
23 /**
24 * Undocumented function
25 *
26 * @param [type] $items
27 * @param [type] $callback($key, $item, $results)
28 * @param [type] $unique_id
29 * @param boolean $split_to_chunks
30 * @return array
31 */
32 public function loop($items, $callback, $unique_id = null, $split_to_chunks = false) {
33 // throw error if the callback is not callable
34 if (!is_callable($callback)) {
35 throw new \Exception('The callback is not callable');
36 }
37 if (!is_array($items)) {
38 throw new \Exception('The items should be an array');
39 }
40
41 // 037 US2 (FR-004): derive the resume identity ONCE here, at a FIXED offset
42 // from loop() (its direct caller), then thread it to the Loop helpers —
43 // instead of each helper re-deriving it via a backtrace level that depends
44 // on the helper-call-chain depth (the removed "adjust level if call depth
45 // changes" foot-gun). The captured values are byte-identical to the prior
46 // scheme: $ctx = the `Class::method` progress/result/skip key (was the
47 // helpers' level-3 derivation); $backup_ctx = the class-only attribute
48 // backup/restore key (was level-4); $sse_ctx = the continue-message key.
49 // Keys are unchanged, so an in-flight import started under the old code
50 // resumes normally — no restart needed (FR-005's safe-restart is moot).
51 $ctx = SessionData::get_calling_identifier($unique_id, true, false, 2);
52 $sse_ctx = SessionData::get_calling_identifier($unique_id, true, false, 3);
53 $backup_ctx = !empty($this->backup_attributes)
54 ? SessionData::get_calling_identifier($unique_id, false, false, 3)
55 : null;
56
57 $results = $this->_get_loop_result([], $ctx);
58
59 if(!empty($this->backup_attributes)){
60 $this->_retrieve_attributes($this->backup_attributes, $backup_ctx);
61 }
62
63 $processed_since_backup = 0;
64
65 foreach ($items as $key => $item) {
66 // If the template has been processed, skip it
67 if ($this->_is_key_processed($key, $ctx)) {
68 continue;
69 }
70
71 // Skip-on-error: Check if feature is enabled and item should be skipped
72 if ($this->_is_skip_feature_enabled()) {
73 $error_attempts = SessionData::get_error_attempts($this->session_id, $ctx, $key);
74
75 // Skip if error attempts >= MAX_ERROR_ATTEMPTS
76 if ($error_attempts >= self::$max_error_attempts) {
77 $this->_mark_key_skipped($key, $ctx, 'Max error attempts reached');
78 $this->_increment_consecutive_skips();
79
80 // Abort the import if too many items are skipped in a row (034 FR-004).
81 // The counter is reset on any successful item (see below).
82 if ($this->_get_consecutive_skips() >= self::$max_consecutive_skips) {
83 throw new FatalErrorException(
84 sprintf(
85 /* translators: %d: number of consecutive skipped items */
86 __('Import stopped: %d items were skipped in a row.', 'templately'),
87 self::$max_consecutive_skips
88 )
89 );
90 }
91
92 continue;
93 }
94 }
95
96 // Wrap callback in try-catch for skip-on-error handling
97 try {
98 $result = $callback($key, $item, $results);
99 if($result === 'continue'){
100 // If the callback returns 'continue', skip to the next iteration
101 continue;
102 }
103 $results = $result;
104
105 // Success - reset consecutive skip counter
106 if ($this->_is_skip_feature_enabled()) {
107 $this->_reset_consecutive_skips();
108 }
109 } catch (SkippableErrorException $e) {
110 // Catchable skip error - increment attempts and mark as skipped
111 if ($this->_is_skip_feature_enabled()) {
112 $this->_increment_error_attempts($key, $ctx);
113 $this->_mark_key_skipped($key, $ctx, $e->getMessage());
114 continue;
115 } else {
116 // Re-throw the ORIGINAL, don't re-wrap it in a bare \Exception.
117 //
118 // Wrapping erased the type: a SkippableErrorException became a plain
119 // Exception, so every catch downstream that branches on the type — and
120 // the SSE layer that derives `retry` from it — lost the classification
121 // this class exists to carry. The message survived; the meaning did not.
122 throw $e;
123 }
124 } catch (Exception $e) {
125 // Other exceptions - increment error attempts then re-throw
126 if ($this->_is_skip_feature_enabled()) {
127 $this->_increment_error_attempts($key, $ctx);
128 }
129 throw $e;
130 }
131
132 // Mark as processed and save result
133 $this->_mark_key_processed($key, $ctx);
134 $this->_set_loop_result($results, $ctx);
135
136 // Persist the runner's in-memory mapping state (backup_attributes —
137 // processed_posts, url_remap, …) every few items, not ONLY at the
138 // graceful chunk-exit: after a hard mid-file death, resume skips the
139 // already-marked items but has NO maps for them, silently mis-wiring
140 // parents/menus/featured images — a corruption class worse than
141 // duplicates.
142 if (!empty($this->backup_attributes) && ++$processed_since_backup >= 5) {
143 $this->_backup_attributes($this->backup_attributes, $backup_ctx);
144 $processed_since_backup = 0;
145 }
146
147 // If it's not the last item, send the SSE message and exit
148
149 $is_last_runner = key( array_slice( $items, -1, 1, true ) ) === $key;
150
151 if( (Helper::fsi_should_exit() || $split_to_chunks) && !$is_last_runner && method_exists($this, 'sse_message') ) {
152 if(!empty($this->backup_attributes)){
153 $this->_backup_attributes($this->backup_attributes, $backup_ctx);
154 }
155 $this->sse_message( [
156 'type' => 'continue',
157 'action' => 'continue',
158 'name' => method_exists($this, 'get_name') ? $this->get_name() : '',
159 'index' => $key,
160 'results' => $sse_ctx,
161 ] );
162 exit;
163 }
164 }
165 return $results;
166 }
167
168 /**
169 * Check if a key has been processed (internal).
170 *
171 * 037 US2: takes the pre-derived progress context (the `Class::method` key
172 * captured once by loop() at a fixed offset) — no backtrace here.
173 *
174 * @param mixed $key The item key
175 * @param string $context The loop progress identity
176 * @return bool True if processed
177 */
178 private function _is_key_processed($key, $context): bool {
179 return SessionData::is_key_processed($this->session_id, $context, $key);
180 }
181
182 /**
183 * Check if a key has been processed (public wrapper). Derives the context at a
184 * FIXED offset from its own direct caller (037 US2 — byte-identical to the old
185 * level-3 derivation reached via this wrapper).
186 */
187 public function is_key_processed($key, $unique_id = null): bool {
188 $context = SessionData::get_calling_identifier($unique_id, true, false, 2);
189 return $this->_is_key_processed($key, $context);
190 }
191
192 /**
193 * Mark a key as processed (internal).
194 *
195 * @param mixed $key The item key
196 * @param string $context The loop progress identity
197 * @return bool Success status
198 */
199 private function _mark_key_processed($key, $context): bool {
200 return SessionData::mark_key_processed($this->session_id, $context, $key);
201 }
202
203 /**
204 * Mark a key as processed (public wrapper). Context from the direct caller.
205 */
206 public function mark_key_processed($key, $unique_id = null): bool {
207 $context = SessionData::get_calling_identifier($unique_id, true, false, 2);
208 return $this->_mark_key_processed($key, $context);
209 }
210
211 /**
212 * Set loop result (internal).
213 *
214 * @param array $result The result data
215 * @param string $context The loop result identity
216 * @return bool Success status
217 */
218 private function _set_loop_result($result, $context): bool {
219 return SessionData::set_loop_result($this->session_id, $context, $result);
220 }
221
222 /**
223 * Set loop result (public wrapper). Context from the direct caller.
224 */
225 public function set_loop_result($result, $unique_id = null): bool {
226 $context = SessionData::get_calling_identifier($unique_id, true, false, 2);
227 return $this->_set_loop_result($result, $context);
228 }
229
230 private function _get_loop_result($defaults, $context) {
231 return SessionData::get_loop_result($this->session_id, $context, $defaults);
232 }
233
234 /**
235 * Get loop result (public wrapper). Context from the direct caller, honoring
236 * the $function flag (several call sites read a class-only key with false).
237 */
238 public function get_loop_result($defaults = [], $unique_id = null, $function = true) {
239 $context = SessionData::get_calling_identifier($unique_id, $function, false, 2);
240 return $this->_get_loop_result($defaults, $context);
241 }
242
243 // Modified get_session_data to use SessionData
244 protected function get_session_data(): array {
245 return SessionData::get_data($this->session_id);
246 }
247
248 // Modified update_session_data to use SessionData (deprecated - use specific methods)
249 protected function update_session_data($data): bool {
250 $existing = SessionData::get_data($this->session_id);
251 return SessionData::save($this->session_id, array_merge($existing, $data));
252 }
253
254 // 037 US2: $backup_context is the class-only key captured once by loop() at a
255 // fixed offset (was each helper's level-4 backtrace) — byte-identical.
256 private function _retrieve_attributes($attributes, $backup_context){
257 $attr_values = SessionData::get($this->session_id, "loop.backup_attributes.{$backup_context}", []);
258
259 foreach ($attributes as $attribute) {
260 if(isset($attr_values[$attribute])){
261 $this->$attribute = $attr_values[$attribute];
262 }
263 }
264 }
265
266 private function _backup_attributes($attributes, $backup_context){
267 $attr_values = [];
268
269 foreach ($attributes as $attribute) {
270 if(isset($this->$attribute)){
271 $attr_values[$attribute] = $this->$attribute;
272 }
273 }
274
275 return SessionData::set($this->session_id, "loop.backup_attributes.{$backup_context}", $attr_values);
276 }
277
278 // ============================================================================
279 // Skip-on-Error Helper Methods
280 // ============================================================================
281
282 /**
283 * Check if skip-on-error feature is enabled
284 *
285 * @return bool True if enabled
286 */
287 protected function _is_skip_feature_enabled(): bool {
288 return (bool) get_option('templately_enable_fsi_skip_on_error', false);
289 }
290
291 /**
292 * Increment error attempts for a loop item
293 *
294 * @param mixed $key The item key
295 * @param string|null $unique_id Optional unique identifier
296 * @return int New error attempt count
297 */
298 // 037 US2: all three take the pre-derived progress context from loop().
299 private function _increment_error_attempts($key, $context): int {
300 return SessionData::increment_error_attempts($this->session_id, $context, $key);
301 }
302
303 /**
304 * Get error attempts for a loop item
305 *
306 * @param mixed $key The item key
307 * @param string $context The loop progress identity
308 * @return int Error attempt count
309 */
310 private function _get_error_attempts($key, $context): int {
311 return SessionData::get_error_attempts($this->session_id, $context, $key);
312 }
313
314 /**
315 * Mark a loop item as skipped
316 *
317 * @param mixed $key The item key
318 * @param string $context The loop progress identity
319 * @param string $reason The reason for skipping
320 * @return bool Success status
321 */
322 private function _mark_key_skipped($key, $context, $reason = ''): bool {
323 return SessionData::mark_key_skipped($this->session_id, $context, $key, $reason);
324 }
325
326 /**
327 * Increment consecutive skip counter
328 *
329 * @return int New consecutive skip count
330 */
331 private function _increment_consecutive_skips(): int {
332 return SessionData::increment_consecutive_skips($this->session_id);
333 }
334
335 /**
336 * Reset consecutive skip counter
337 *
338 * @return bool Success status
339 */
340 private function _reset_consecutive_skips(): bool {
341 return SessionData::reset_consecutive_skips($this->session_id);
342 }
343
344 /**
345 * Get consecutive skip count
346 *
347 * @return int Consecutive skip count
348 */
349 private function _get_consecutive_skips(): int {
350 return SessionData::get_consecutive_skips($this->session_id);
351 }
352
353 }