queue()) { return; } if (null !== WC()->queue()->get_next(self::UPDATE_HOOK, array(), self::UPDATE_GROUP)) { return; } $running = WC()->queue()->search(array( 'hook' => self::UPDATE_HOOK, 'status' => \ActionScheduler_Store::STATUS_RUNNING, 'per_page' => 1, 'group' => self::UPDATE_GROUP, ), 'ids'); if (!empty($running)) { return; } WC()->queue()->add(self::UPDATE_HOOK, array(), self::UPDATE_GROUP); } public static function run_as_callback() { if (self::run()) { self::maybe_schedule(); } } public static function run(): bool { if (get_option(self::COMPLETED_OPTION)) { return false; } $variation_pending = self::migrate_variation_batch(); $term_pending = self::migrate_swatch_terms_batch(); if (!$variation_pending && !$term_pending) { self::promote_attribute_types(); update_option(self::COMPLETED_OPTION, time(), false); return false; } return true; } private static function migrate_variation_batch(): bool { global $wpdb; $variation_ids = $wpdb->get_col($wpdb->prepare( "SELECT legacy.post_id FROM {$wpdb->postmeta} AS legacy INNER JOIN {$wpdb->posts} AS posts ON posts.ID = legacy.post_id AND posts.post_type = 'product_variation' LEFT JOIN {$wpdb->postmeta} AS canonical ON canonical.post_id = legacy.post_id AND canonical.meta_key = '_product_image_gallery' AND canonical.meta_value <> '' LEFT JOIN {$wpdb->postmeta} AS attempted ON attempted.post_id = legacy.post_id AND attempted.meta_key = %s AND attempted.meta_value = 'yes' WHERE legacy.meta_key = 'botiga_variation_gallery' AND legacy.meta_value <> '' AND canonical.post_id IS NULL AND attempted.post_id IS NULL GROUP BY legacy.post_id ORDER BY legacy.post_id ASC LIMIT %d", self::MIGRATED_META, self::BATCH_SIZE )); if (empty($variation_ids)) { return false; } foreach ($variation_ids as $vid) { self::migrate_one_variation((int) $vid); } return count($variation_ids) >= self::BATCH_SIZE; } public static function migrate_one_variation(int $variation_id): void { $legacy = get_post_meta($variation_id, 'botiga_variation_gallery', true); if (empty($legacy) || !is_array($legacy)) { update_post_meta($variation_id, self::MIGRATED_META, 'yes'); return; } $image_ids = array(); $videos = array(); foreach ($legacy as $position => $entry) { $classified = self::classify_gallery_entry($entry); if ($classified === null) { continue; } if ($classified['kind'] === 'image') { $image_ids[] = $classified['id']; } else { $videos[] = array_merge($classified, array('position' => (int) $position)); } } if (!empty($image_ids)) { update_post_meta($variation_id, '_product_image_gallery', implode(',', $image_ids)); $existing_thumb = (int) get_post_meta($variation_id, '_thumbnail_id', true); if (!$existing_thumb || !wp_attachment_is_image($existing_thumb)) { update_post_meta($variation_id, '_thumbnail_id', $image_ids[0]); } } if (!empty($videos)) { update_post_meta($variation_id, '_aieo_variation_gallery_videos', $videos); } update_post_meta($variation_id, self::SENTINEL_META, 'yes'); update_post_meta($variation_id, self::MIGRATED_META, 'yes'); } public static function classify_gallery_entry($entry): ?array { if (is_numeric($entry)) { $id = (int) $entry; if ($id <= 0) { return null; } if (wp_attachment_is('video', $id)) { return array('kind' => 'video', 'type' => 'attachment', 'id' => $id); } if (wp_attachment_is_image($id)) { return array('kind' => 'image', 'id' => $id); } return null; } if (is_string($entry) && (strpos($entry, 'youtube') !== false || strpos($entry, 'youtu.be') !== false)) { $obj = json_decode($entry); if ($obj && isset($obj->url)) { return array( 'kind' => 'video', 'type' => 'youtube', 'url' => (string) $obj->url, 'aspect_ratio' => isset($obj->aspect_ratio) ? (string) $obj->aspect_ratio : '16-9', ); } return null; } if (is_string($entry) && strpos($entry, 'vimeo') !== false) { $obj = json_decode($entry); if ($obj && isset($obj->url)) { return array( 'kind' => 'video', 'type' => 'vimeo', 'url' => (string) $obj->url, 'aspect_ratio' => isset($obj->aspect_ratio) ? (string) $obj->aspect_ratio : '16-9', ); } return null; } return null; } private static function migrate_swatch_terms_batch(): bool { global $wpdb; $term_ids = $wpdb->get_col($wpdb->prepare( "SELECT DISTINCT tm.term_id FROM {$wpdb->termmeta} AS tm INNER JOIN {$wpdb->term_taxonomy} AS tt ON tt.term_id = tm.term_id LEFT JOIN {$wpdb->termmeta} AS canonical ON canonical.term_id = tm.term_id AND canonical.meta_key = 'color' AND canonical.meta_value <> '' LEFT JOIN {$wpdb->termmeta} AS sidecar_type ON sidecar_type.term_id = tm.term_id AND sidecar_type.meta_key = '_aieo_swatch_color_type' LEFT JOIN {$wpdb->termmeta} AS sidecar_image ON sidecar_image.term_id = tm.term_id AND sidecar_image.meta_key = '_aieo_swatch_image_id' LEFT JOIN {$wpdb->termmeta} AS attempted ON attempted.term_id = tm.term_id AND attempted.meta_key = %s AND attempted.meta_value = 'yes' WHERE tm.meta_key IN ('product_attribute_color', 'product_attribute_image') AND tm.meta_value <> '' AND tt.taxonomy LIKE %s AND canonical.term_id IS NULL AND sidecar_type.term_id IS NULL AND sidecar_image.term_id IS NULL AND attempted.term_id IS NULL ORDER BY tm.term_id ASC LIMIT %d", self::TERM_MIGRATED_META, $wpdb->esc_like('pa_') . '%', self::BATCH_SIZE )); if (empty($term_ids)) { return false; } foreach ($term_ids as $term_id) { self::migrate_one_term((int) $term_id); } return count($term_ids) >= self::BATCH_SIZE; } public static function migrate_one_term(int $term_id): void { $type = (string) get_term_meta($term_id, 'product_attribute_color_type', true); $bg_color = (string) get_term_meta($term_id, 'product_attribute_color', true); $grad_c1 = (string) get_term_meta($term_id, 'product_attribute_color_gradient_color_1', true); $grad_c2 = (string) get_term_meta($term_id, 'product_attribute_color_gradient_color_2', true); $grad_dir = (string) get_term_meta($term_id, 'product_attribute_color_gradient_direction', true); $multi_dir = (string) get_term_meta($term_id, 'product_attribute_color_multiple_direction', true); $multi_colors = array_filter(array( (string) get_term_meta($term_id, 'product_attribute_color_multiple_color_1', true), (string) get_term_meta($term_id, 'product_attribute_color_multiple_color_2', true), (string) get_term_meta($term_id, 'product_attribute_color_multiple_color_3', true), (string) get_term_meta($term_id, 'product_attribute_color_multiple_color_4', true), )); $image_id = (int) get_term_meta($term_id, 'product_attribute_image', true); $canonical_hex = ''; if ($type === 'gradient' && $grad_c1 !== '') { $canonical_hex = $grad_c1; } elseif ($type === 'multiple' && !empty($multi_colors)) { $canonical_hex = reset($multi_colors); } elseif ($bg_color !== '') { $canonical_hex = $bg_color; } if ($canonical_hex !== '') { $sanitised = sanitize_hex_color($canonical_hex); if ($sanitised) { update_term_meta($term_id, 'color', $sanitised); } } if ($type !== '' && $type !== 'background') { update_term_meta($term_id, '_aieo_swatch_color_type', $type); if ($type === 'gradient' && ($grad_c1 !== '' || $grad_c2 !== '')) { update_term_meta($term_id, '_aieo_swatch_gradient', array( 'color_1' => $grad_c1, 'color_2' => $grad_c2, 'direction' => $grad_dir, )); } elseif ($type === 'multiple' && !empty($multi_colors)) { update_term_meta($term_id, '_aieo_swatch_multiple', array( 'colors' => array_values($multi_colors), 'direction' => $multi_dir, )); } } if ($image_id > 0) { update_term_meta($term_id, '_aieo_swatch_image_id', $image_id); } update_term_meta($term_id, self::TERM_MIGRATED_META, 'yes'); } private static function promote_attribute_types(): void { global $wpdb; $table = $wpdb->prefix . 'woocommerce_attribute_taxonomies'; $wpdb->query("UPDATE {$table} SET attribute_type = 'wc-visual' WHERE attribute_type = 'color'"); delete_transient('wc_attribute_taxonomies'); } /** * @when after_wp_load */ public static function cli_run($args, $assoc_args) { $force = !empty($assoc_args['force']); if ($force) { delete_option(self::COMPLETED_OPTION); } $batch = 0; do { $batch++; if (class_exists('WP_CLI')) { WP_CLI::log("Running batch {$batch}..."); } $pending = self::run(); } while ($pending); if (class_exists('WP_CLI')) { WP_CLI::success(sprintf('AIEO→WC compat migration complete after %d batch(es).', $batch)); } } }