Skip to content

Instantly share code, notes, and snippets.

@batonac
Created October 30, 2024 00:31
Show Gist options
  • Save batonac/aa68537be27630f9ec08ac76b5deff43 to your computer and use it in GitHub Desktop.
Save batonac/aa68537be27630f9ec08ac76b5deff43 to your computer and use it in GitHub Desktop.
WooCommerce Subscriptions Variations Fixer (after migration)
<?php
class SubscriptionVariationFixer {
private $proposed_changes = [];
private $total_items = 0;
private $total_issues = 0;
// Helper function to get the current site's attribute format from a variation
private function get_variation_attributes($variation_id) {
global $wpdb;
$attributes = [];
$var_attributes = $wpdb->get_results($wpdb->prepare("
SELECT meta_key, meta_value
FROM {$wpdb->postmeta}
WHERE post_id = %d
AND meta_key LIKE 'attribute_pa_%'
", $variation_id));
foreach ($var_attributes as $attr) {
// Convert attribute_pa_something to pa_something
$key = preg_replace('/^attribute_/', '', $attr->meta_key);
$attributes[$key] = $attr->meta_value;
}
return $attributes;
}
// Helper function to normalize keys and values
private function normalize_attribute($key, $value) {
// Handle key normalization
$normalized_key = preg_replace('/^pa_/', '', $key);
$normalized_key = ($normalized_key === 'package') ? 'packaging' : $normalized_key;
$normalized_key = 'pa_' . $normalized_key;
// Handle value normalization
$normalized_value = preg_replace('/-coffee$/', '', $value);
$normalized_value = preg_replace('/^whole-/', '', $normalized_value);
return [
'key' => $normalized_key,
'value' => $normalized_value
];
}
public function execute() {
global $wpdb;
try {
WP_CLI::log("Starting attribute normalization...");
// First get all order items with their attributes
$items = $wpdb->get_results("
SELECT
items.order_item_id,
items.order_id,
items.order_item_name,
meta_product.meta_value as product_id,
meta_variation.meta_value as variation_id
FROM {$wpdb->prefix}woocommerce_order_items as items
JOIN {$wpdb->prefix}wc_orders as orders ON items.order_id = orders.id
JOIN {$wpdb->prefix}woocommerce_order_itemmeta as meta_product
ON items.order_item_id = meta_product.order_item_id AND meta_product.meta_key = '_product_id'
LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta as meta_variation
ON items.order_item_id = meta_variation.order_item_id AND meta_variation.meta_key = '_variation_id'
WHERE orders.type = 'shop_subscription'
AND items.order_item_type = 'line_item'
");
foreach ($items as $item) {
$this->total_items++;
// Get all attribute meta for this item
$attribute_meta = $wpdb->get_results($wpdb->prepare("
SELECT meta_id, meta_key, meta_value
FROM {$wpdb->prefix}woocommerce_order_itemmeta
WHERE order_item_id = %d
AND meta_key LIKE 'pa_%'
", $item->order_item_id));
if (empty($attribute_meta)) {
continue;
}
// Get the correct attribute format from the current variation
$correct_attributes = $this->get_variation_attributes($item->variation_id);
$needs_update = false;
$updates = [];
foreach ($attribute_meta as $meta) {
$normalized = $this->normalize_attribute($meta->meta_key, $meta->meta_value);
// Check if the key needs updating
if ($meta->meta_key !== $normalized['key']) {
$needs_update = true;
$updates[] = [
'meta_id' => $meta->meta_id,
'type' => 'key',
'old' => $meta->meta_key,
'new' => $normalized['key']
];
}
// Check if the value needs updating
if ($meta->meta_value !== $normalized['value']) {
$needs_update = true;
$updates[] = [
'meta_id' => $meta->meta_id,
'type' => 'value',
'old' => $meta->meta_value,
'new' => $normalized['value']
];
}
}
if ($needs_update) {
$this->total_issues++;
$this->proposed_changes[] = [
'item_id' => $item->order_item_id,
'subscription_id' => $item->order_id,
'product_name' => $item->order_item_name,
'updates' => $updates
];
}
}
WP_CLI::log("\nAnalysis Summary:");
WP_CLI::log("Total items processed: " . $this->total_items);
WP_CLI::log("Items with attribute issues: " . $this->total_issues);
WP_CLI::log("Items to update: " . count($this->proposed_changes));
if (!empty($this->proposed_changes)) {
foreach ($this->proposed_changes as $change) {
WP_CLI::log(sprintf(
"\nSubscription #%d - %s",
$change['subscription_id'],
$change['product_name']
));
foreach ($change['updates'] as $update) {
WP_CLI::log(sprintf(
" %s update: %s → %s",
$update['type'],
$update['old'],
$update['new']
));
}
}
WP_CLI::confirm("Do you want to apply these changes?");
$changes_made = 0;
foreach ($this->proposed_changes as $change) {
foreach ($change['updates'] as $update) {
if ($update['type'] === 'key') {
$wpdb->update(
$wpdb->prefix . 'woocommerce_order_itemmeta',
['meta_key' => $update['new']],
['meta_id' => $update['meta_id']]
);
} else {
$wpdb->update(
$wpdb->prefix . 'woocommerce_order_itemmeta',
['meta_value' => $update['new']],
['meta_id' => $update['meta_id']]
);
}
}
$changes_made++;
}
WP_CLI::success(sprintf("Successfully updated attributes for %d items!", $changes_made));
} else {
WP_CLI::success("No attribute updates needed!");
}
} catch (Exception $e) {
WP_CLI::error($e->getMessage());
return 1;
}
}
}
// Run the fixer
$fixer = new SubscriptionVariationFixer();
$fixer->execute();
<?php
class SubscriptionVariationFixer {
private $proposed_changes = [];
private $total_items = 0;
private $total_issues = 0;
// Helper function to normalize attribute value
private function normalize_attribute_value($value) {
return preg_replace('/-coffee$/', '', $value);
}
// Helper function to normalize attribute key
private function normalize_attribute_key($key) {
$key = preg_replace('/^(attribute_)?pa_/', '', $key);
return ($key === 'package') ? 'packaging' : $key;
}
// Helper function to check if attributes match
private function attributes_match($order_attributes, $variation_attributes) {
foreach ($order_attributes as $key => $value) {
$normalized_key = $this->normalize_attribute_key($key);
$normalized_value = $this->normalize_attribute_value($value);
$found_match = false;
foreach ($variation_attributes as $var_attr) {
if ($this->normalize_attribute_key($var_attr->meta_key) === $normalized_key) {
if ($this->normalize_attribute_value($var_attr->meta_value) === $normalized_value) {
$found_match = true;
break;
}
}
}
if (!$found_match) {
return false;
}
}
return true;
}
// Helper function to get correct attribute values from variation
private function get_variation_attribute_values($variation_id) {
global $wpdb;
$attributes = [];
$var_attributes = $wpdb->get_results($wpdb->prepare("
SELECT meta_key, meta_value
FROM {$wpdb->postmeta}
WHERE post_id = %d
AND meta_key LIKE '%pa_%'
", $variation_id));
foreach ($var_attributes as $attr) {
$key = preg_replace('/^attribute_/', '', $attr->meta_key);
$attributes[$key] = $attr->meta_value;
}
return $attributes;
}
public function execute() {
global $wpdb;
try {
WP_CLI::log("Starting subscription variation and attribute analysis...");
// Get all subscriptions from HPOS
$subscriptions = $wpdb->get_results("
SELECT id
FROM {$wpdb->prefix}wc_orders
WHERE type = 'shop_subscription'
");
if (empty($subscriptions)) {
throw new Exception("No subscriptions found");
}
foreach ($subscriptions as $subscription) {
// Get line items
$line_items = $wpdb->get_results($wpdb->prepare("
SELECT *
FROM {$wpdb->prefix}woocommerce_order_items
WHERE order_id = %d
AND order_item_type = 'line_item'
", $subscription->id));
foreach ($line_items as $item) {
$this->total_items++;
// Get item meta
$meta = $wpdb->get_results($wpdb->prepare("
SELECT meta_key, meta_value
FROM {$wpdb->prefix}woocommerce_order_itemmeta
WHERE order_item_id = %d
", $item->order_item_id));
// Extract product, variation IDs and attributes
$product_id = null;
$variation_id = null;
$attributes = [];
$attribute_meta_ids = [];
foreach ($meta as $m) {
if ($m->meta_key === '_product_id') {
$product_id = $m->meta_value;
} elseif ($m->meta_key === '_variation_id') {
$variation_id = $m->meta_value;
} elseif (strpos($m->meta_key, 'pa_') === 0) {
$attributes[$m->meta_key] = $m->meta_value;
$attribute_meta_ids[$m->meta_key] = $m->meta_id;
}
}
if (!$product_id || empty($attributes)) {
continue;
}
// Get all variations for this product
$variations = $wpdb->get_results($wpdb->prepare("
SELECT ID
FROM {$wpdb->posts}
WHERE post_parent = %d
AND post_type = 'product_variation'
AND post_status = 'publish'
", $product_id));
$matching_variation = null;
foreach ($variations as $var) {
$var_attributes = $wpdb->get_results($wpdb->prepare("
SELECT meta_key, meta_value
FROM {$wpdb->postmeta}
WHERE post_id = %d
AND meta_key LIKE '%pa_%'
", $var->ID));
if ($this->attributes_match($attributes, $var_attributes)) {
$matching_variation = $var->ID;
break;
}
}
if ($matching_variation) {
$needs_update = false;
$attribute_updates = [];
// Check if variation ID needs updating
if ($matching_variation !== $variation_id) {
$needs_update = true;
}
// Get correct attribute values from matching variation
$correct_attributes = $this->get_variation_attribute_values($matching_variation);
// Compare current attributes with correct ones
foreach ($attributes as $key => $value) {
$normalized_value = $this->normalize_attribute_value($value);
$correct_key = 'pa_' . $this->normalize_attribute_key($key);
if (isset($correct_attributes[$correct_key]) &&
$normalized_value !== $correct_attributes[$correct_key]) {
$needs_update = true;
$attribute_updates[$key] = [
'meta_id' => $attribute_meta_ids[$key],
'old_value' => $value,
'new_value' => $correct_attributes[$correct_key]
];
}
}
if ($needs_update) {
$this->total_issues++;
$this->proposed_changes[] = [
'item_id' => $item->order_item_id,
'subscription_id' => $subscription->id,
'product_id' => $product_id,
'old_variation_id' => $variation_id,
'new_variation_id' => $matching_variation,
'product_name' => $item->order_item_name,
'attribute_updates' => $attribute_updates
];
}
}
}
}
WP_CLI::log("\nAnalysis Summary:");
WP_CLI::log("Total items processed: " . $this->total_items);
WP_CLI::log("Items with variation or attribute issues: " . $this->total_issues);
WP_CLI::log("Fixes found: " . count($this->proposed_changes));
if (!empty($this->proposed_changes)) {
foreach ($this->proposed_changes as $change) {
WP_CLI::log(sprintf(
"\nSubscription #%d - %s\nVariation ID: %d → %d",
$change['subscription_id'],
$change['product_name'],
$change['old_variation_id'],
$change['new_variation_id']
));
if (!empty($change['attribute_updates'])) {
WP_CLI::log("Attribute Updates:");
foreach ($change['attribute_updates'] as $key => $update) {
WP_CLI::log(sprintf(
" %s: %s → %s",
$key,
$update['old_value'],
$update['new_value']
));
}
}
}
WP_CLI::confirm("Do you want to apply these changes?");
// Apply changes
$changes_made = 0;
foreach ($this->proposed_changes as $change) {
// Update variation ID
$wpdb->update(
$wpdb->prefix . 'woocommerce_order_itemmeta',
['meta_value' => $change['new_variation_id']],
[
'order_item_id' => $change['item_id'],
'meta_key' => '_variation_id'
]
);
// Update attributes
foreach ($change['attribute_updates'] as $key => $update) {
$wpdb->update(
$wpdb->prefix . 'woocommerce_order_itemmeta',
['meta_value' => $update['new_value']],
['meta_id' => $update['meta_id']]
);
}
$changes_made++;
WP_CLI::log(sprintf(
"Updated item #%d in subscription #%d",
$change['item_id'],
$change['subscription_id']
));
}
WP_CLI::success(sprintf("Successfully applied %d changes!", $changes_made));
} else {
WP_CLI::success("No changes needed!");
}
} catch (Exception $e) {
WP_CLI::error($e->getMessage());
return 1;
}
}
}
// Run the fixer
$fixer = new SubscriptionVariationFixer();
$fixer->execute();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment