PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.4.2
ShopBuilder – WooCommerce Builder For Elementor v3.4.2
3.4.2 3.4.1 3.4.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 All 63 releases
shopbuilder / app / Modules / VariationGallery / GalleryMigration.php

GalleryMigration.php in ShopBuilder – WooCommerce Builder For Elementor 3.4.2, at app/Modules/VariationGallery/GalleryMigration.php

242 lines 6.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Batched migration of the legacy variation gallery meta into WooCommerce's
4 * native per-variation gallery.
5 *
6 * @package RadiusTheme\SB
7 */
8
9 namespace RadiusTheme\SB\Modules\VariationGallery;
10
11 use RadiusTheme\SB\Traits\SingletonTrait;
12
13 defined( 'ABSPATH' ) || exit();
14
15 /**
16 * Copies `rtsb_vg_images` into WooCommerce's `_product_image_gallery` variation meta
17 * so the native gallery becomes the single source of truth once WooCommerce provides
18 * it.
19 *
20 * The legacy meta is deliberately left in place: it keeps CSV export/import and any
21 * third-party reader working, and lets a store roll back to an older WooCommerce
22 * without losing data. A sentinel meta marks each processed variation instead, so the
23 * legacy value is never used to resurrect images the merchant has since removed.
24 *
25 * The work runs through Action Scheduler (shipped with WooCommerce, so no new
26 * dependency) in small batches: it survives the admin closing the page or logging out,
27 * never blocks a request, and resumes safely after an interrupted batch.
28 */
29 class GalleryMigration {
30
31 /**
32 * Singleton Trait.
33 */
34 use SingletonTrait;
35
36 /**
37 * Option recording when the migration finished.
38 *
39 * @var string
40 */
41 const COMPLETED_OPTION = 'rtsb_vg_native_gallery_migration_completed_at';
42
43 /**
44 * Action Scheduler hook running a single batch.
45 *
46 * @var string
47 */
48 const BATCH_HOOK = 'rtsb_vg_migrate_variation_gallery_batch';
49
50 /**
51 * Action Scheduler group.
52 *
53 * @var string
54 */
55 const BATCH_GROUP = 'rtsb-variation-gallery';
56
57 /**
58 * Number of variations processed per batch.
59 *
60 * @var int
61 */
62 const BATCH_SIZE = 250;
63
64 /**
65 * Transient guarding against two runners entering a batch at once.
66 *
67 * @var string
68 */
69 const LOCK_TRANSIENT = 'rtsb_vg_migration_running';
70
71 /**
72 * Register hooks.
73 */
74 private function __construct() {
75 add_action( self::BATCH_HOOK, [ __CLASS__, 'run_batch' ] );
76 // Action Scheduler only becomes available on `init`.
77 add_action( 'init', [ $this, 'maybe_schedule' ], 21 );
78 }
79
80 /**
81 * Queue the next batch unless the migration is done or one is already queued.
82 *
83 * @return void
84 */
85 public function maybe_schedule() {
86 if ( ! GalleryFns::use_native_gallery() || get_option( self::COMPLETED_OPTION ) ) {
87 return;
88 }
89
90 $queue = self::get_queue();
91
92 if ( ! $queue || null !== $queue->get_next( self::BATCH_HOOK, [], self::BATCH_GROUP ) ) {
93 return;
94 }
95
96 $queue->add( self::BATCH_HOOK, [], self::BATCH_GROUP );
97 }
98
99 /**
100 * Migrate one batch of variations and queue the next one when work remains.
101 *
102 * Idempotent by construction: the batch query only selects variations that still
103 * lack the sentinel, so a batch that dies part-way simply leaves the remainder to
104 * be picked up on the next run — nothing is processed twice and nothing is skipped.
105 *
106 * @return void
107 */
108 public static function run_batch() {
109 if ( ! GalleryFns::use_native_gallery() || get_option( self::COMPLETED_OPTION ) ) {
110 return;
111 }
112
113 // Two runners racing on the same batch would each write the same values, but
114 // the lock keeps them from duplicating the queued follow-up batch as well.
115 if ( get_transient( self::LOCK_TRANSIENT ) ) {
116 return;
117 }
118
119 set_transient( self::LOCK_TRANSIENT, 'yes', 5 * MINUTE_IN_SECONDS );
120
121 try {
122 $variation_ids = self::get_pending_variation_ids( self::BATCH_SIZE );
123
124 foreach ( $variation_ids as $variation_id ) {
125 self::migrate_variation( $variation_id );
126 }
127
128 $has_more = (bool) self::get_pending_variation_ids( 1 );
129 } finally {
130 delete_transient( self::LOCK_TRANSIENT );
131 }
132
133 if ( $has_more ) {
134 $queue = self::get_queue();
135
136 if ( $queue ) {
137 $queue->add( self::BATCH_HOOK, [], self::BATCH_GROUP );
138 }
139
140 return;
141 }
142
143 if ( ! get_option( self::COMPLETED_OPTION ) ) {
144 update_option( self::COMPLETED_OPTION, time(), false );
145 }
146 }
147
148 /**
149 * Copy one variation's legacy gallery into the native gallery.
150 *
151 * The native value wins whenever it already holds images, so a merchant who has
152 * authored through WooCommerce before the migration ran is never overwritten.
153 *
154 * @param int $variation_id Variation ID.
155 *
156 * @return void
157 */
158 protected static function migrate_variation( $variation_id ) {
159 $variation_id = absint( $variation_id );
160 $variation = $variation_id ? wc_get_product( $variation_id ) : false;
161
162 if ( ! $variation || ! $variation->is_type( 'variation' ) ) {
163 return;
164 }
165
166 /*
167 * Re-check ownership at write time, not just in the batch query.
168 *
169 * A batch selects up to BATCH_SIZE IDs in one statement and then processes them
170 * one by one, so a merchant can save this variation in between. Were they to
171 * empty its gallery in that window, the native store would be legitimately empty
172 * while the legacy meta still held the old list — and the write below would put
173 * the removed images straight back.
174 */
175 if ( GalleryFns::is_native_gallery_owned( $variation_id ) ) {
176 return;
177 }
178
179 $legacy_ids = GalleryFns::get_legacy_variation_gallery_ids( $variation_id );
180 $native_ids = GalleryFns::get_native_variation_gallery_ids( $variation );
181
182 if ( empty( $native_ids ) && ! empty( $legacy_ids ) ) {
183 $variation->set_gallery_image_ids( $legacy_ids );
184 $variation->save();
185 }
186
187 GalleryFns::mark_native_gallery_owned( $variation_id );
188
189 // The cached props were built from the legacy list; drop them so the next read
190 // rebuilds from the native gallery.
191 GalleryFns::delete_transients( $variation_id, 'variation-images' );
192 }
193
194 /**
195 * Fetch variation IDs that still carry an unmigrated legacy gallery.
196 *
197 * @param int $limit Maximum number of IDs to return.
198 *
199 * @return array
200 */
201 protected static function get_pending_variation_ids( $limit ) {
202 global $wpdb;
203
204 $query = $wpdb->prepare(
205 "SELECT legacy.post_id
206 FROM {$wpdb->postmeta} AS legacy
207 INNER JOIN {$wpdb->posts} AS posts
208 ON posts.ID = legacy.post_id
209 AND posts.post_type = 'product_variation'
210 LEFT JOIN {$wpdb->postmeta} AS migrated
211 ON migrated.post_id = legacy.post_id
212 AND migrated.meta_key = %s
213 WHERE legacy.meta_key = %s
214 AND legacy.meta_value <> ''
215 AND legacy.meta_value <> 'a:0:{}'
216 AND migrated.post_id IS NULL
217 GROUP BY legacy.post_id
218 ORDER BY legacy.post_id ASC
219 LIMIT %d",
220 GalleryFns::NATIVE_GALLERY_SENTINEL_META_KEY,
221 GalleryFns::LEGACY_GALLERY_META_KEY,
222 absint( $limit )
223 );
224
225 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Prepared immediately above; a migration scan must not be cached.
226 return array_map( 'absint', $wpdb->get_col( $query ) );
227 }
228
229 /**
230 * Resolve WooCommerce's Action Scheduler queue when it is available.
231 *
232 * @return \WC_Queue_Interface|null
233 */
234 protected static function get_queue() {
235 if ( ! function_exists( 'WC' ) || ! method_exists( WC(), 'queue' ) ) {
236 return null;
237 }
238
239 return WC()->queue();
240 }
241 }
242