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 / charitable / importer.php

importer.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.6.0, at inc/import/charitable/importer.php

208 lines 6.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable migration orchestrator.
4 *
5 * Walks a session through its phases by delegating each batch to the
6 * appropriate phase mapper. Mappers are registered via the filter
7 * `suredonation_import_charitable_phase_mappers` so the Pro plugin can plug
8 * in subscription / standalone-donor mappers without Free knowing
9 * about them.
10 *
11 * @package SureDonation
12 */
13
14 namespace SureDonation\Inc\Import\Charitable;
15
16 use SureDonation\Inc\Traits\Get_Instance;
17
18 // Exit if accessed directly.
19 defined( 'ABSPATH' ) || exit;
20
21 /**
22 * Importer class.
23 *
24 * @since 1.0.0
25 */
26 class Importer {
27 use Get_Instance;
28
29 /**
30 * Records processed per AJAX batch. Matches Charitable's 25.
31 */
32 public const BATCH_SIZE = 25;
33
34 /**
35 * Get pre-flight counts and per-gateway breakdown.
36 *
37 * @return array{has_data:bool, charitable_version:?string, counts:array<string,int>, gateway_breakdown:array<int,array<string,mixed>>}
38 * @since 1.0.0
39 */
40 public function get_counts() {
41 $source = Source::get_instance();
42
43 $gateway_breakdown = [];
44 foreach ( $source->get_gateway_breakdown() as $row ) {
45 $slug = isset( $row['slug'] ) ? (string) $row['slug'] : '';
46 $count = isset( $row['count'] ) ? (int) $row['count'] : 0;
47 $sd_slug = Status_Map::map_gateway( $slug );
48 $live = Status_Map::is_gateway_live( $sd_slug );
49 $sub_live = Status_Map::is_subscription_handler_live( $sd_slug );
50 $gateway_breakdown[] = [
51 'slug' => $slug,
52 'sd_slug' => $sd_slug,
53 'count' => $count,
54 'live' => $live,
55 'subscription_handler_live' => $sub_live,
56 ];
57 }
58
59 // When the Charitable tables were never created, short-circuit the
60 // counts that query them (donations / standalone donors) so we don't
61 // emit "table doesn't exist" DB errors during the pre-flight read.
62 $has_data = $source->has_charitable_data();
63
64 return [
65 'has_data' => $has_data,
66 'charitable_version' => $source->get_charitable_version(),
67 'counts' => [
68 'campaigns' => $source->get_campaign_count(),
69 'donations' => $has_data ? $source->get_donation_count() : 0,
70 'subscriptions' => $source->get_subscription_count(),
71 'standalone_donors' => $has_data ? $source->get_standalone_donor_count() : 0,
72 ],
73 'gateway_breakdown' => $gateway_breakdown,
74 ];
75 }
76
77 /**
78 * Run the next batch for a given session.
79 *
80 * Loads progress, dispatches the current phase to its mapper, persists
81 * progress, and advances the phase index when the mapper signals the
82 * phase is exhausted (processed < batch size).
83 *
84 * @param string $import_id Session UUID.
85 * @return array<string, mixed>|false Updated progress payload, or false if the session is missing or no longer running.
86 * @since 1.0.0
87 */
88 public function run_batch( $import_id ) {
89 $session = Session::get_instance();
90 $progress = $session->get( $import_id );
91
92 if ( ! is_array( $progress ) || empty( $progress['status'] ) || 'running' !== $progress['status'] ) {
93 return false;
94 }
95
96 $current_index = isset( $progress['current_phase'] ) && is_numeric( $progress['current_phase'] ) ? (int) $progress['current_phase'] : 0;
97 $phases = isset( $progress['phases'] ) && is_array( $progress['phases'] ) ? $progress['phases'] : [];
98
99 if ( $current_index >= count( $phases ) ) {
100 $progress['status'] = 'complete';
101 $session->put( $import_id, $progress );
102 $this->fire_batch_complete( $import_id, $progress );
103 return $progress;
104 }
105
106 $phase = (string) $phases[ $current_index ];
107 $offset = isset( $progress['offset'] ) && is_numeric( $progress['offset'] ) ? (int) $progress['offset'] : 0;
108
109 $mapper = $this->get_phase_mapper( $phase );
110
111 try {
112 if ( ! $mapper || ! is_callable( [ $mapper, 'process_batch' ] ) ) {
113 // Unknown phase — skip it and advance so the session never stalls.
114 $progress['current_phase'] = $current_index + 1;
115 $progress['offset'] = 0;
116 } else {
117 $processed = (int) $mapper->process_batch( $progress, $offset );
118
119 if ( $processed < self::BATCH_SIZE ) {
120 $progress['current_phase'] = $current_index + 1;
121 $progress['offset'] = 0;
122 } else {
123 $progress['offset'] = $offset + $processed;
124 }
125 }
126 } catch ( \Throwable $t ) {
127 // Whole-batch failure (mapper threw something the per-record
128 // try/catch didn't catch, or a fatal in the dispatcher).
129 // Mark the session as failed so the import history doesn't
130 // sit at 'running' forever, persist the cause for diagnosis,
131 // and short-circuit further phase advancement.
132 $progress['status'] = 'failed';
133 $progress['completed_at'] = current_time( 'mysql', true );
134 $progress['error'] = $t->getMessage();
135 $session->put( $import_id, $progress );
136 $this->fire_batch_complete( $import_id, $progress );
137 return $progress;
138 }
139
140 if ( $progress['current_phase'] >= count( $phases ) ) {
141 $progress['status'] = 'complete';
142 $progress['completed_at'] = current_time( 'mysql', true );
143 }
144
145 $session->put( $import_id, $progress );
146
147 $this->fire_batch_complete( $import_id, $progress );
148
149 return $progress;
150 }
151
152 /**
153 * Resolve the mapper instance responsible for a given phase.
154 *
155 * @param string $phase Phase name.
156 * @return object|null Mapper with a process_batch( &$progress, $offset ) method.
157 * @since 1.0.0
158 */
159 private function get_phase_mapper( $phase ) {
160 $mappers = [
161 'campaigns' => Campaign_Mapper::class,
162 'donations' => Donation_Mapper::class,
163 ];
164
165 /**
166 * Filter the map of phase => mapper class.
167 *
168 * Pro plugin registers `subscriptions` and `standalone_donors` mappers
169 * here. Each class must expose a public process_batch( &$progress, $offset )
170 * method that returns the number of source records processed in the batch.
171 *
172 * @param array<string,string> $mappers Phase => mapper class.
173 * @since 1.0.0
174 */
175 $mappers = apply_filters( 'suredonation_import_charitable_phase_mappers', $mappers );
176
177 if ( ! is_array( $mappers ) || ! isset( $mappers[ $phase ] ) ) {
178 return null;
179 }
180
181 $class = (string) $mappers[ $phase ];
182 if ( ! class_exists( $class ) || ! is_callable( [ $class, 'get_instance' ] ) ) {
183 return null;
184 }
185
186 return call_user_func( [ $class, 'get_instance' ] );
187 }
188
189 /**
190 * Fire the batch-complete action so observers (e.g. Pro history recorder) can persist snapshots.
191 *
192 * @param string $import_id Session UUID.
193 * @param array<string, mixed> $progress Progress payload after this batch.
194 * @return void
195 * @since 1.0.0
196 */
197 private function fire_batch_complete( $import_id, $progress ) {
198 /**
199 * Fires after each migration batch is processed (running or final).
200 *
201 * @param string $import_id Session UUID.
202 * @param array $progress Updated progress payload.
203 * @since 1.0.0
204 */
205 do_action( 'suredonation_import_charitable_batch_complete', $import_id, $progress );
206 }
207 }
208