PluginProbe ʕ •ᴥ•ʔ
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments / 4.5.0
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments v4.5.0
4.5.1 4.5.0 4.4.2 4.4.1 4.4.0 4.3.3 4.3.2 4.3.1 4.3.0 4.2.3 4.2.2 4.2.1 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.11.0 1.11.1 1.11.2 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.9.0 1.9.1 1.9.2 1.9.3 1.9.4 1.9.5 2.0.0 2.0.1 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.10.0 2.10.1 2.11.0 2.11.1 2.11.2 2.11.3 2.11.4 2.12.0 2.13.0 2.14.0 2.14.1 2.15.0 2.15.1 2.16.0 2.16.1 2.16.2 2.16.3 2.17.0 2.17.1 2.17.2 2.18.0 2.19.0 2.19.2 2.19.3 2.19.4 2.2.0 2.2.1 2.20.0 2.20.1 2.20.2 2.20.3 2.20.4 2.20.5 2.20.6 2.21.0 2.22.0 2.22.1 2.23.0 2.24.0 2.25.0 2.25.1 2.25.2 2.26.0 2.27.0 2.27.1 2.28.0 2.29.0 2.29.1 2.29.2 2.29.3 2.29.4 2.3.0 2.3.1 2.30.0 2.31.0 2.31.1 2.31.2 2.31.3 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.40.0 2.40.1 2.5.0 2.5.1 2.5.2 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.9.0 3.0.0 3.0.0-RC1 3.0.0-RC2 3.0.0-beta1 3.0.0-beta2 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.10.0 3.10.1 3.11.0 3.12.0 3.13.0 3.13.1 3.13.2 3.13.3 3.13.4 3.14.0 3.15.0 3.15.1 3.15.2 3.15.3 3.15.4 3.15.5 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.16.5 3.16.6 3.16.7 3.16.8 3.17.0 3.17.1 3.17.2 3.17.3 3.17.4 3.17.5 3.17.6 3.18.0 3.19.0 3.19.1 3.19.2 3.2.0 3.2.1 3.2.2 3.20.0 3.20.1 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 4.0.0 4.0.1 4.0.2 4.0.3 trunk 4.1.0 0.2.19.1 4.1.1 1.0.0 4.2.0 1.0.1 1.0.2
surecart / app / src / Sync / WooCommerce / WooCommerceImportService.php
surecart / app / src / Sync / WooCommerce Last commit date
WooCommerceImportCleanupService.php 2 months ago WooCommerceImportJob.php 2 months ago WooCommerceImportService.php 2 weeks ago WooCommerceImportTask.php 2 months ago WooCommerceProductMapper.php 2 months ago
WooCommerceImportService.php
331 lines
1 <?php
2
3 namespace SureCart\Sync\WooCommerce;
4
5 use SureCart\Sync\ImportState;
6
7 /**
8 * Thin service for WooCommerce product import.
9 *
10 * Handles bootstrapping (admin notices), dispatching the background job,
11 * and providing import status/count queries.
12 *
13 * @package SureCart
14 */
15 class WooCommerceImportService {
16
17 /**
18 * Application instance.
19 *
20 * @var \SureCart\Application
21 */
22 protected $app;
23
24 /**
25 * WooCommerce import state tracker.
26 *
27 * @var ImportState
28 */
29 protected $import_state;
30
31 /**
32 * Cached running state for this request.
33 *
34 * Both admin_notices callbacks call isRunning(), which runs a wp_options
35 * query plus Action Scheduler lookups. Memoize so that chain runs once per request.
36 *
37 * @var bool|null
38 */
39 private $is_running = null;
40
41 /**
42 * Constructor.
43 *
44 * @param \SureCart\Application $app The application.
45 * @param ImportState $import_state Import state for WooCommerce runs.
46 */
47 public function __construct( $app, ImportState $import_state ) {
48 $this->app = $app;
49 $this->import_state = $import_state;
50 }
51
52 /**
53 * Bootstrap admin notices.
54 *
55 * @return void
56 */
57 public function bootstrap() {
58 add_action( 'admin_notices', [ $this, 'showSyncNotice' ] );
59 add_action( 'admin_notices', [ $this, 'showCompletionNotice' ] );
60 }
61
62 /**
63 * Get the background import job instance.
64 *
65 * @return WooCommerceImportJob
66 */
67 private function job() {
68 return $this->app->resolve( 'surecart.jobs.woo_import' );
69 }
70
71 /**
72 * Is the import currently running?
73 *
74 * @return boolean
75 */
76 public function isRunning() {
77 if ( null === $this->is_running ) {
78 $this->is_running = $this->job()->isRunning();
79 }
80 return $this->is_running;
81 }
82
83 /**
84 * Dispatch the import job.
85 *
86 * Only one import can run at a time — isRunning() prevents concurrent dispatches.
87 * WordPress options (sc_woo_import_ids, sc_woo_import_session_id) are used for
88 * per-import state tracking, which is safe under this single-import constraint.
89 *
90 * @param int $batch_size Products per batch page.
91 *
92 * @return array|\WP_Error
93 */
94 public function dispatch( $batch_size = 100 ) {
95 // Clear previously accumulated import IDs and session tracking.
96 $this->import_state->reset();
97
98 $batch_size = apply_filters( 'surecart/sync/woocommerce_products/batch_size', $batch_size );
99 $batch_size = max( 1, min( 500, (int) $batch_size ) );
100
101 return $this->job()->push_to_queue(
102 [
103 'page' => 1,
104 'batch_size' => $batch_size,
105 ]
106 )->save()->dispatch();
107 }
108
109 /**
110 * Get the count of importable (not yet imported) WooCommerce products.
111 *
112 * Uses pre-fetched imported IDs with 'exclude' parameter instead of
113 * a slow NOT EXISTS meta query.
114 *
115 * @return int
116 */
117 public function getImportableCount() {
118 if ( ! class_exists( 'WooCommerce' ) ) {
119 return 0;
120 }
121
122 // Purge stale import flags (throttled — runs once every 6 hours).
123 $this->purgeStaleImportMeta();
124
125 // Pre-fetch already-imported product IDs (EXISTS meta query is faster than NOT EXISTS).
126 $imported_ids = get_posts(
127 [
128 'post_type' => 'product',
129 'meta_key' => '_surecart_imported', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
130 'fields' => 'ids',
131 'numberposts' => -1,
132 ]
133 );
134
135 $query_args = [
136 'limit' => 1,
137 'page' => 1,
138 'return' => 'ids',
139 'paginate' => true,
140 ];
141
142 if ( ! empty( $imported_ids ) ) {
143 $query_args['exclude'] = $imported_ids;
144 }
145
146 $products = wc_get_products( $query_args );
147
148 if ( ! is_object( $products ) || ! isset( $products->total ) ) {
149 return 0;
150 }
151
152 return (int) $products->total;
153 }
154
155 /**
156 * Cross-reference WC import flags against local sc_product posts.
157 * Clears stale _surecart_imported meta for WC products whose
158 * corresponding SC product no longer exists locally.
159 *
160 * Throttled to run at most once every 6 hours.
161 *
162 * @return void
163 */
164 protected function purgeStaleImportMeta() {
165 // Throttle: skip if already checked recently.
166 if ( get_transient( 'sc_woo_import_purge_checked' ) ) {
167 return;
168 }
169
170 // 1. Get all WC products marked as imported.
171 $imported_wc_ids = get_posts(
172 [
173 'post_type' => 'product',
174 'meta_key' => '_surecart_imported', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
175 'fields' => 'ids',
176 'numberposts' => -1,
177 ]
178 );
179
180 // Mark as checked (even if nothing to do).
181 set_transient( 'sc_woo_import_purge_checked', true, 6 * HOUR_IN_SECONDS );
182
183 if ( empty( $imported_wc_ids ) ) {
184 return;
185 }
186
187 // 2. Single DB query: get all wc_product_ids from sc_product posts.
188 $valid_wc_ids = $this->getValidImportedWcIds();
189
190 // 3. Find stale entries.
191 $stale_wc_ids = array_diff( $imported_wc_ids, $valid_wc_ids );
192
193 if ( empty( $stale_wc_ids ) ) {
194 return;
195 }
196
197 // 4. Clear stale import flags.
198 foreach ( $stale_wc_ids as $wc_id ) {
199 delete_post_meta( (int) $wc_id, '_surecart_imported' );
200 }
201
202 // 5. Invalidate cached excluded IDs.
203 delete_transient( 'sc_woo_import_excluded_ids' );
204 }
205
206 /**
207 * Single DB query to extract wc_product_id values from sc_product post meta.
208 * Uses LIKE to skip non-WC products at the DB level.
209 *
210 * @return int[] Valid WC product IDs that still have a local SC product.
211 */
212 protected function getValidImportedWcIds() {
213 global $wpdb;
214
215 // One query: join posts + postmeta, filter to sc_product with wc_product_id.
216 $rows = $wpdb->get_col( $wpdb->prepare(
217 "SELECT pm.meta_value
218 FROM {$wpdb->postmeta} pm
219 JOIN {$wpdb->posts} p ON p.ID = pm.post_id
220 WHERE p.post_type = 'sc_product'
221 AND pm.meta_key = 'product'
222 AND pm.meta_value LIKE %s",
223 '%' . $wpdb->esc_like( 'wc_product_id' ) . '%'
224 ) );
225
226 $valid_ids = [];
227 foreach ( $rows as $serialized ) {
228 $data = maybe_unserialize( $serialized );
229
230 // Extract metadata — may be object or array at any nesting level.
231 $metadata = is_object( $data ) ? ( $data->metadata ?? null ) : ( $data['metadata'] ?? null );
232 $wc_id = is_object( $metadata ) ? ( $metadata->wc_product_id ?? null ) : ( is_array( $metadata ) ? ( $metadata['wc_product_id'] ?? null ) : null );
233
234 if ( ! empty( $wc_id ) ) {
235 $valid_ids[] = (int) $wc_id;
236 }
237 }
238
239 return $valid_ids;
240 }
241
242 /**
243 * Show an admin notice if products are being imported.
244 *
245 * @return void
246 */
247 public function showSyncNotice() {
248 // Don't show on the import results page -- the user is already viewing results.
249 if ( 'import_results' === ( isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
250 return;
251 }
252
253 if ( ! $this->isRunning() ) {
254 return;
255 }
256
257 echo wp_kses_post(
258 \SureCart::notices()->render(
259 [
260 'type' => 'info',
261 'title' => esc_html__( 'SureCart: WooCommerce products import in progress.', 'surecart' ),
262 'text' => '<p>' . esc_html__( 'SureCart is importing WooCommerce products in the background. The process may take a little while, so please be patient.', 'surecart' ) . '</p>',
263 ]
264 )
265 );
266 }
267
268 /**
269 * Show a completion notice after import finishes.
270 *
271 * @return void
272 */
273 public function showCompletionNotice() {
274 // Don't show on the import results page -- the user is already viewing results.
275 if ( 'import_results' === ( isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
276 return;
277 }
278
279 // don't show if import is still running.
280 if ( $this->isRunning() ) {
281 return;
282 }
283
284 $state = $this->import_state;
285 $import_ids = $state->getResultIds();
286 $all_skipped_session_id = $state->getAllSkippedSessionId();
287
288 // Lazily detect the all-skipped case: no imports created but skipped products exist.
289 // This mutation lives in the notice renderer (not in Job::complete()) because the Job
290 // finishes before tasks complete — tasks run async via Action Scheduler, so result IDs
291 // aren't available yet at Job::complete() time. The write is idempotent (same session_id
292 // every time), so concurrent admin page loads are harmless.
293 if ( empty( $import_ids ) && ! $all_skipped_session_id ) {
294 if ( $state->getSessionId() && ! empty( $state->getSkippedItems() ) ) {
295 $state->markAllSkipped();
296 $all_skipped_session_id = $state->getAllSkippedSessionId();
297 }
298 }
299
300 // Generate results URL and notice name per branch.
301 $notice_name = '';
302 if ( ! empty( $import_ids ) ) {
303 // Normal case: has import_ids.
304 $results_url = \SureCart::getUrl()->importResults( 'products', $import_ids, $state->getSessionId() );
305 $notice_name = 'woo_import_complete_' . md5( implode( ',', $import_ids ) );
306 } elseif ( $all_skipped_session_id ) {
307 // All-skipped case: use session_id.
308 $results_url = \SureCart::getUrl()->importResultsBySession( 'products', $all_skipped_session_id );
309 $notice_name = 'woo_import_complete_' . md5( $all_skipped_session_id );
310 } else {
311 // No data available, skip notice.
312 return;
313 }
314
315 echo wp_kses_post(
316 \SureCart::notices()->render(
317 [
318 'name' => $notice_name,
319 'type' => 'success',
320 'title' => esc_html__( 'SureCart: WooCommerce products import complete.', 'surecart' ),
321 'text' => '<p>' . sprintf(
322 /* translators: %s: URL to import results page */
323 __( 'The import has finished. <a href="%s">View Import Results</a>', 'surecart' ),
324 esc_url( $results_url )
325 ) . '</p>',
326 ]
327 )
328 );
329 }
330 }
331