PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.6.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.6.0
1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
suredonation / inc / import-export / import / import-runner.php

import-runner.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.6.0, at inc/import-export/import/import-runner.php

376 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Own-data import runner: transient session store + batch orchestrator.
4 *
5 * Self-contained sibling of the GiveWP migration importer — it mirrors the
6 * proven batched-session pattern (BATCH_SIZE chunks, `process_batch()` returns
7 * the number of source rows handled, `< BATCH_SIZE` ends the phase) but with
8 * its own transient prefix, results schema, and phase-mapper filter. It does
9 * NOT touch the GiveWP importer.
10 *
11 * @package SureDonation
12 * @since 1.3.0
13 */
14
15 namespace SureDonation\Inc\Import_Export\Import;
16
17 use SureDonation\Inc\Helper;
18
19 // Exit if accessed directly.
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit;
22 }
23
24 /**
25 * Import session store + orchestrator.
26 *
27 * @since 1.3.0
28 */
29 class Import_Runner {
30
31 /**
32 * Transient key prefix for import sessions.
33 *
34 * @var string
35 * @since 1.3.0
36 */
37 const TRANSIENT_PREFIX = 'suredonation_import_';
38
39 /**
40 * Session lifetime.
41 *
42 * @var int
43 * @since 1.3.0
44 */
45 const TTL = HOUR_IN_SECONDS;
46
47 /**
48 * Rows processed per batch.
49 *
50 * @var int
51 * @since 1.3.0
52 */
53 const BATCH_SIZE = 25;
54
55 /**
56 * Create a new import session.
57 *
58 * @param string $entity 'donations' or 'donors'.
59 * @param string $token Stored CSV token.
60 * @param array<int, string> $mapping Header index => field.
61 * @param array<string, mixed> $options Import options (dry_run, mode, ...).
62 * @param int $total_rows Total data rows.
63 * @return array<string, mixed> The created progress payload.
64 * @since 1.3.0
65 */
66 public static function create( $entity, $token, $mapping, $options, $total_rows ) {
67 $import_id = wp_generate_uuid4();
68
69 $progress = [
70 'import_id' => $import_id,
71 'started_at' => current_time( 'mysql', true ),
72 'started_by' => get_current_user_id(),
73 'entity' => $entity,
74 'token' => $token,
75 'mapping' => $mapping,
76 'options' => $options,
77 'phases' => [ $entity ],
78 'current_phase' => 0,
79 'offset' => 0,
80 'byte_offset' => 0,
81 'total_rows' => (int) $total_rows,
82 'donor_map' => [],
83 'campaign_map' => [],
84 'id_map' => [],
85 'created' => [
86 'donations' => [],
87 'donors' => [],
88 ],
89 'status' => 'running',
90 'results' => [ $entity => self::empty_result() ],
91 ];
92
93 self::put( $import_id, $progress );
94
95 /**
96 * Fires when an import session is created (before the first batch).
97 * Pro uses this to open an import-history record.
98 *
99 * @param string $import_id Session id.
100 * @param array<string, mixed> $progress Session payload.
101 */
102 do_action( 'suredonation_import_session_created', $import_id, $progress );
103
104 return $progress;
105 }
106
107 /**
108 * Empty per-phase results counters.
109 *
110 * @return array<string, mixed>
111 * @since 1.3.0
112 */
113 private static function empty_result() {
114 return [
115 'imported' => 0,
116 'skipped' => 0,
117 'errors' => 0,
118 'donors_created' => 0,
119 'donors_matched' => 0,
120 'error_log' => [],
121 ];
122 }
123
124 /**
125 * Record a created row id in the session (for rollback), keyed by type.
126 *
127 * @param array<string, mixed> $progress Session (by reference).
128 * @param string $type 'donations' or 'donors'.
129 * @param int $id Created row id.
130 * @return void
131 * @since 1.3.0
132 */
133 public static function track_created( &$progress, $type, $id ) {
134 if ( (int) $id <= 0 ) {
135 return;
136 }
137 $created = is_array( $progress['created'] ?? null ) ? $progress['created'] : [];
138 $list = is_array( $created[ $type ] ?? null ) ? $created[ $type ] : [];
139 $list[] = (int) $id;
140
141 $created[ $type ] = $list;
142 $progress['created'] = $created;
143 }
144
145 /**
146 * Record an old-id => new-id mapping in the session (for relinking recurring
147 * renewals to their new parent donation on completion).
148 *
149 * @param array<string, mixed> $progress Session (by reference).
150 * @param int $old_id Original (source) id.
151 * @param int $new_id New id.
152 * @return void
153 * @since 1.3.0
154 */
155 public static function track_id_map( &$progress, $old_id, $new_id ) {
156 if ( (int) $old_id <= 0 ) {
157 return;
158 }
159 $map = is_array( $progress['id_map'] ?? null ) ? $progress['id_map'] : [];
160 $map[ (int) $old_id ] = (int) $new_id;
161 $progress['id_map'] = $map;
162 }
163
164 /**
165 * Read a session.
166 *
167 * @param string $import_id Session id.
168 * @return array<string, mixed>|false Progress payload, or false.
169 * @since 1.3.0
170 */
171 public static function get( $import_id ) {
172 $id = self::sanitize_id( $import_id );
173 if ( '' === $id ) {
174 return false;
175 }
176 $progress = get_transient( self::TRANSIENT_PREFIX . $id );
177 return is_array( $progress ) ? $progress : false;
178 }
179
180 /**
181 * Write a session.
182 *
183 * @param string $import_id Session id.
184 * @param array<string, mixed> $progress Progress payload.
185 * @return bool
186 * @since 1.3.0
187 */
188 public static function put( $import_id, $progress ) {
189 $id = self::sanitize_id( $import_id );
190 if ( '' === $id ) {
191 return false;
192 }
193 return set_transient( self::TRANSIENT_PREFIX . $id, $progress, self::TTL );
194 }
195
196 /**
197 * Delete a session.
198 *
199 * @param string $import_id Session id.
200 * @return bool
201 * @since 1.3.0
202 */
203 public static function delete( $import_id ) {
204 $id = self::sanitize_id( $import_id );
205 if ( '' === $id ) {
206 return false;
207 }
208 return delete_transient( self::TRANSIENT_PREFIX . $id );
209 }
210
211 /**
212 * Validate an import id.
213 *
214 * @param string $import_id Candidate id.
215 * @return string Valid id, or '' if invalid.
216 * @since 1.3.0
217 */
218 private static function sanitize_id( $import_id ) {
219 return is_string( $import_id ) && preg_match( '/^[a-f0-9\-]{8,64}$/i', $import_id ) ? $import_id : '';
220 }
221
222 /**
223 * Process the next batch for a session.
224 *
225 * @param string $import_id Session id.
226 * @return array<string, mixed>|false Updated progress, or false if the session isn't runnable.
227 * @since 1.3.0
228 */
229 public static function run_batch( $import_id ) {
230 $progress = self::get( $import_id );
231 if ( false === $progress || 'running' !== ( $progress['status'] ?? '' ) ) {
232 return false;
233 }
234
235 $phases = is_array( $progress['phases'] ?? null ) ? $progress['phases'] : [];
236 $index = Helper::get_integer_value( $progress['current_phase'] ?? 0 );
237
238 if ( $index >= count( $phases ) ) {
239 return self::finish( $import_id, $progress );
240 }
241
242 $phase = Helper::get_string_value( $phases[ $index ] ?? '' );
243 $offset = Helper::get_integer_value( $progress['offset'] ?? 0 );
244 $mapper = self::get_phase_mapper( $phase );
245
246 try {
247 if ( null === $mapper || ! is_callable( [ $mapper, 'process_batch' ] ) ) {
248 $progress['current_phase'] = $index + 1;
249 $progress['offset'] = 0;
250 $progress['byte_offset'] = 0;
251 } else {
252 // The mapper advances 'byte_offset' by reference as it reads.
253 $processed = (int) $mapper->process_batch( $progress, $offset );
254 if ( $processed < self::BATCH_SIZE ) {
255 $progress['current_phase'] = $index + 1;
256 $progress['offset'] = 0;
257 $progress['byte_offset'] = 0;
258 } else {
259 $progress['offset'] = $offset + $processed;
260 }
261 }
262 } catch ( \Throwable $t ) {
263 // The raw message can carry filesystem paths and the table prefix, and
264 // it is both returned to the client and persisted in the session, so
265 // log it and hand back a generic failure instead.
266 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
267 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug-only diagnostic for a failed import.
268 error_log( 'SureDonation import ' . $import_id . ' failed: ' . $t->getMessage() );
269 }
270
271 $progress['status'] = 'failed';
272 $progress['completed_at'] = current_time( 'mysql', true );
273 $progress['error'] = __( 'The import stopped because of an unexpected error. Please try again.', 'suredonation' );
274 self::cleanup_file( $progress );
275 self::put( $import_id, $progress );
276 return $progress;
277 }
278
279 if ( Helper::get_integer_value( $progress['current_phase'] ?? 0 ) >= count( $phases ) ) {
280 return self::finish( $import_id, $progress );
281 }
282
283 self::put( $import_id, $progress );
284
285 /**
286 * Fires after each import batch. Pro uses this to update the
287 * import-history record's progress.
288 *
289 * @param string $import_id Session id.
290 * @param array<string, mixed> $progress Session payload.
291 */
292 do_action( 'suredonation_import_batch_complete', $import_id, $progress );
293
294 return $progress;
295 }
296
297 /**
298 * Mark a session complete and persist.
299 *
300 * @param string $import_id Session id.
301 * @param array<string, mixed> $progress Progress payload.
302 * @return array<string, mixed>
303 * @since 1.3.0
304 */
305 private static function finish( $import_id, $progress ) {
306 $progress['status'] = 'complete';
307 $progress['completed_at'] = current_time( 'mysql', true );
308 self::cleanup_file( $progress );
309 self::put( $import_id, $progress );
310
311 // Action documented in this class's run_batch().
312 do_action( 'suredonation_import_batch_complete', $import_id, $progress );
313
314 /**
315 * Fires once when an import session completes. Pro uses this to finalize
316 * the history record and remap recurring renewal → parent links.
317 *
318 * @param string $import_id Session id.
319 * @param array<string, mixed> $progress Final session payload.
320 */
321 do_action( 'suredonation_import_complete', $import_id, $progress );
322
323 return $progress;
324 }
325
326 /**
327 * Delete the session's uploaded CSV once it is no longer needed.
328 *
329 * The file holds donor PII (emails, names, phones); it is removed as soon
330 * as the run completes or fails so it does not linger in uploads.
331 *
332 * @param array<string, mixed> $progress Progress payload.
333 * @return void
334 * @since 1.3.0
335 */
336 private static function cleanup_file( $progress ) {
337 $token = Helper::get_string_value( $progress['token'] ?? '' );
338 if ( '' !== $token ) {
339 Csv_File::delete( $token );
340 }
341 }
342
343 /**
344 * Resolve a phase to its mapper singleton.
345 *
346 * @param string $phase Phase name.
347 * @return object|null Mapper instance, or null.
348 * @since 1.3.0
349 */
350 private static function get_phase_mapper( $phase ) {
351 $mappers = [
352 'donations' => Donations_Import_Mapper::class,
353 'donors' => Donors_Import_Mapper::class,
354 ];
355
356 /**
357 * Filter the import phase => mapper-class map. Pro registers its own
358 * phases (e.g. subscriptions) here.
359 *
360 * @param array<string, string> $mappers Phase => fully-qualified class name.
361 */
362 $mappers = apply_filters( 'suredonation_import_phase_mappers', $mappers );
363
364 if ( ! isset( $mappers[ $phase ] ) ) {
365 return null;
366 }
367
368 $class = (string) $mappers[ $phase ];
369 if ( ! class_exists( $class ) || ! is_callable( [ $class, 'get_instance' ] ) ) {
370 return null;
371 }
372
373 return $class::get_instance();
374 }
375 }
376