Large WooCommerce query throws fatal memory errors - wordpress

I have a site that has tens of thousands of orders, which I need to compare the billing and customer/user emails and show a flag if they don't match. One of the stipulations is that I'm unable to add any metadata to the orders. So my solution is to just add a custom column, and compare the emails on the fly when the orders list is rendered. That works just fine.
add_filter( 'manage_edit-shop_order_columns', 'mismatched_orders_column' );
function mismatched_orders_column( $columns ) {
$columns['mismatched'] = 'Mismatched';
return $columns;
}
add_action( 'manage_shop_order_posts_custom_column', 'mismatched_orders_column_data' );
function mismatched_orders_column_data( $column ) {
global $post;
if ( 'mismatched' === $column ) {
$order = new WC_Order( $post->ID );
$customer = $order->get_user();
$result = '';
$billing_email = strtolower ( $order->get_billing_email() );
$customer_email = '';
if ($customer) $customer_email = strtolower ( $customer->user_email );
if ( $customer && ( $billing_email != $customer_email ) ) {
$result = '<span class="mismatched-order" title="Possible order mismatch">Yes</span>';
}
echo $result;
}
}
My issue is when trying to add sorting. Because I'm not accessing any post metadata, I don't have any easy data to sort via the main query. My solution here was originally to hook into pre_get_posts, grab all the orders in a new WP_Query, then loop through them and add the ones that had mismatched emails to an array for use in post__in.
This works/worked fine on my small dev site, but throws fatal memory errors when trying to loop over any more than about 8 or 9 thousand posts (out of a total of 30-40 thousand). Increasing memory isn't really an option.
add_filter( 'manage_edit-shop_order_sortable_columns', 'mismatched_orders_column_sortable');
function mismatched_orders_column_sortable( $columns ) {
$columns['mismatched'] = 'mismatched';
return $columns;
}
add_action( 'pre_get_posts', 'mismatched_emails_posts_orderby' );
function mismatched_emails_posts_orderby( $query ) {
if( ! is_admin() || ! $query->is_main_query() ) {
return;
}
//Remove the pre_get_posts hook so we don't get stuck in a loop
add_action( 'pre_get_posts', 'mismatched_emails_posts_orderby' );
//Make sure we're only looking at our custom column
if ( 'mismatched' === $query->get( 'orderby') ) {
//Set our initial array for 'post__in'
$mismatched = array();
$orders_list = get_posts(array(
'post_type' => 'shop_order',
'posts_per_page' => -1,
'post_status' => 'any',
'fields' => 'ids'
));
//And here is our problem
foreach( $orders_list as $order_post ) :
//Get our order and customer/user object
$order_object = new WC_Order( $order_post );
$customer = $order_object->get_user();
//Check that billing and customer emails don't match, and also that we're not dealing with a guest order
if ( ( $order_object->get_billing_email() != $customer->user_email ) && $order_object->get_user() != false ) {
$mismatched[] = $order_post;
}
endforeach; wp_reset_postdata();
$query->set( 'post__in', $mismatched );
}
}
I would seriously appreciate any insight into how I could either reduce the expense of the query I'm trying to run, or an alternate approach. Again, just for clarification, adding metadata to the orders isn't an option.
Thanks!

Related

How do I write a foreach loop that checks how many times a user bought a specific item in Woocommerce?

I'm using this code to generate an iframe if a customer has bought product 13372 and it works great:
<?php
// Get the current user data:
$user = wp_get_current_user();
$user_id = $user->ID; // Get the user ID
$customer_email = $user->user_email; // Get the user email
// OR
// $customer_email = get_user_meta( $user->ID, 'billing_email', true ); // Get the user billing email
// The conditional function (example)
// IMPORTANT: $product_id argument need to be defined
$product_id = 13372;
if( wc_customer_bought_product( $customer_email, $user_id, $product_id ) ) {
echo "You have additional listings!";
//iFrame goes here
} else {
echo "You have no additional listings.";
}
?>
Now I need to modify this to check how many times a user bought product ID 13372 and output that many number of iframes. If bought 3 times, output 3 iframes. I'm assuming a foreach loop, but what I've tried doesn't work. I followed this post: How to check how many times a product has been bought by a customer
But the example it doesn't return anything for me, not sure why!
Please try the following. We're looping through each completed shop order of a specific customer and putting all IDs of purchased products into an array. We then loop through that array to see how many times a specific ID appears. We store that value in a $count variable, which is used to determine how many times to output an iframe.
I've commented the code and made function and variable names as clear as possible.
<?php
function so58032512_get_product_purchase_quantity( $product_id ) {
// Array to hold ids of all products purchased by customer
$product_ids = [];
// Get all customer orders
$customer_orders = get_posts( array(
'numberposts' => - 1,
'meta_key' => '_customer_user',
'meta_value' => get_current_user_id(),
'post_type' => 'shop_order', // WC orders post type
'post_status' => 'wc-completed' // Only orders with status "completed"
) );
// Loop through each of this customer's order
foreach ( $customer_orders as $customer_order ) {
$order = wc_get_order( $customer_order );
$items = $order->get_items();
// Loop through each product in order and add its ID to $product_ids array
foreach ( $items as $item ) {
$id = $item['product_id'];
array_push( $product_ids, $id );
}
}
// Variable to count times an ID exists in the $product_ids array
$count = 0;
// Loop through all of the IDs in our $product_ids array
foreach ( $product_ids as $key => $value ) {
// Every time the ID that we're checking against appears in the array
if ( $value == $product_id ) {
// Increment our counter
$count ++;
}
}
// Return the counter value that represents the number of times the
// customer bought a product with our passed in ID ($product_id)
return $count;
}
// Get the current user data:
$user = wp_get_current_user();
$user_id = $user->ID; // Get the user ID
$customer_email = $user->user_email; // Get the user email
// IMPORTANT: $product_id argument need to be defined
$product_id = 13372;
if ( wc_customer_bought_product( $customer_email, $user_id, $product_id ) ) {
$x = 1;
$number_of_iframes_to_show = so58032512_get_product_purchase_quantity( $product_id );
echo 'The customer bought product with ID ' . $product_id . ' ' . $number_of_iframes_to_show . ' time(s).<br>';
while ( $x <= $number_of_iframes_to_show ) {
echo "<br>Here's an iframe!<br>";
$x ++;
}
} else {
echo "You have no additional listings.";
}
?>

WooCommerce, how to remove downloadable products permission for order

I would like to remove the permission to download the files of certain orders (even if already paid), e.g. if customer payment gets revoked.
I am fiddling around with "wc_downloadable_file_permission" but somehow this only grants permission, but I want to revoke it.
I even tried to manually handle wp_posts_meta (_download_permissions_granted:yes/no) but it did not work.
So far i got this.
function revoke_download_permission( $order_id ) {
$order = wc_get_order( $order_id );
if ( sizeof( $order->get_items() ) > 0 ) {
foreach ( $order->get_items() as $item ) {
$product = $item->get_product();
if ( $product && $product->exists() && $product->is_downloadable() ) {
$downloads = $product->get_downloads();
foreach ( array_keys( $downloads ) as $download_id ) {
wc_downloadable_file_permission( $download_id, $product, $order, $item->get_quantity() );
}
}
}
}
}
Juergen's solution is not consistent with WooCommerce's new datastore mechanism as of this writing. Additionally, it does not remove download permissions from the download-permission database that do not have a corresponding order.
Here's an improved solution for regenerating all download permissions in bulk, that addresses these issues as well. The below is consistent with the "Regenerate download permissions" command in WooCommerce, but it is applied to all completed orders.
Put this in your functions.php file, load the wp-admin page, and then remove it from functions.php.
Obvious caveat: THIS WILL DELETE ALL DOWNLOAD PERMISSIONS FOR YOUR SITE before regenerating them. If you've manually assigned permissions to any customer for any product, those will be hosed and made consistent with your order database. Use totally at your own risk, no warranties expressed or implied.
add_action('init','my_activate_download_permissions');
function my_activate_download_permissions() {
$orders = get_posts( array(
'post_type' => 'shop_order',
'post_status' => 'wc-completed',
'posts_per_page' => -1
) );
foreach ( $orders as $order ) {
$data_store = WC_Data_Store::load( 'customer-download' );
$data_store->delete_by_order_id( $order->ID );
wc_downloadable_product_permissions( $order->ID, true );
}
}
I think, i got it.
Instead of completely removing the download permission I set the value of remaining downloads to zero which has the same effect. And if customer pays you can still set the value back to it's default value.
function revoke_download_permission( $order_id ) {
global $wpdb;
$order = wc_get_order( $order_id );
if ( sizeof( $order->get_items() ) > 0 ) {
foreach ( $order->get_items() as $item ) {
$product = $item->get_product();
if ( $product && $product->exists() && $product->is_downloadable() ) {
$downloads = $product->get_downloads();
foreach ( array_keys( $downloads ) as $download_id ) {
$wpdb->update(
'wp_woocommerce_downloadable_product_permissions',
array(
'downloads_remaining' => '0'
),
array( 'download_id' => $download_id )
);
}
}
}
}
}
After search for a whole day, found a solution to do this with a query in MYSQL (i used 5.7) (big thanks to #Tim Biegeleisen)
To delete all permission downloads from selected ORDERS ID:
DELETE
FROM wp_woocommerce_downloadable_product_permissions
WHERE `order_id` IN (ORDER ID1, ORDER ID2, ...);
If you want to delete permission for all refunded orders:
DELETE
FROM wp_woocommerce_downloadable_product_permissions
WHERE EXISTS (
SELECT 1 FROM WHERE wp_posts.ID = wp_woocommerce_downloadable_product_permissions.order_id
AND wp_posts.post_status = 'wc-refunded');
The solutions posted didn't suit my needs. This is the way I would do it, it actually deletes the download permissions from the order directly.
function revoke_download_permissions( $order_id, $product_id )
{
$data_store = new WC_Customer_Download_Data_Store;
$order = wc_get_order( $order_id );
if ( sizeof( $order->get_items() ) > 0) {
foreach ( $order->get_items() as $item ) {
$product = $item->get_product();
if ( $product && $product->exists() && $product->is_downloadable() ) {
if ( $product->get_id() == $product_id) {
$downloads = $product->get_downloads();
foreach ( $downloads as $d ) {
$data_store->delete_by_download_id( $d->get_id() );
}
}
}
}
}
}

WooCommerce: how to add multiple products to cart at once?

I need a "get products A, B and C for $xxx" special offer, products A, B and C must be available on their own, and the bundle is a special offer accessible through a coupon code.
On a marketing page hosting outside my site, I would like a button leading to my site that carries a query string like ?add-to-cart=244,249,200 so that once on my site, all bundle products are already added to the cart (instead of adding them one by one which sounds unacceptably tedious).
If not possible, then at least I'd like a landing page on my site with a single button adding all bundle products to cart at once.
I couldn't find working solutions googling around (here's one example). Any suggestion?
After some research I found that DsgnWrks wrote a hook that does exactly this. For your convenience, and in case the blog goes offline, I bluntly copied his code to this answer:
function woocommerce_maybe_add_multiple_products_to_cart( $url = false ) {
// Make sure WC is installed, and add-to-cart qauery arg exists, and contains at least one comma.
if ( ! class_exists( 'WC_Form_Handler' ) || empty( $_REQUEST['add-to-cart'] ) || false === strpos( $_REQUEST['add-to-cart'], ',' ) ) {
return;
}
// Remove WooCommerce's hook, as it's useless (doesn't handle multiple products).
remove_action( 'wp_loaded', array( 'WC_Form_Handler', 'add_to_cart_action' ), 20 );
$product_ids = explode( ',', $_REQUEST['add-to-cart'] );
$count = count( $product_ids );
$number = 0;
foreach ( $product_ids as $id_and_quantity ) {
// Check for quantities defined in curie notation (<product_id>:<product_quantity>)
// https://dsgnwrks.pro/snippets/woocommerce-allow-adding-multiple-products-to-the-cart-via-the-add-to-cart-query-string/#comment-12236
$id_and_quantity = explode( ':', $id_and_quantity );
$product_id = $id_and_quantity[0];
$_REQUEST['quantity'] = ! empty( $id_and_quantity[1] ) ? absint( $id_and_quantity[1] ) : 1;
if ( ++$number === $count ) {
// Ok, final item, let's send it back to woocommerce's add_to_cart_action method for handling.
$_REQUEST['add-to-cart'] = $product_id;
return WC_Form_Handler::add_to_cart_action( $url );
}
$product_id = apply_filters( 'woocommerce_add_to_cart_product_id', absint( $product_id ) );
$was_added_to_cart = false;
$adding_to_cart = wc_get_product( $product_id );
if ( ! $adding_to_cart ) {
continue;
}
$add_to_cart_handler = apply_filters( 'woocommerce_add_to_cart_handler', $adding_to_cart->get_type(), $adding_to_cart );
// Variable product handling
if ( 'variable' === $add_to_cart_handler ) {
woo_hack_invoke_private_method( 'WC_Form_Handler', 'add_to_cart_handler_variable', $product_id );
// Grouped Products
} elseif ( 'grouped' === $add_to_cart_handler ) {
woo_hack_invoke_private_method( 'WC_Form_Handler', 'add_to_cart_handler_grouped', $product_id );
// Custom Handler
} elseif ( has_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler ) ){
do_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler, $url );
// Simple Products
} else {
woo_hack_invoke_private_method( 'WC_Form_Handler', 'add_to_cart_handler_simple', $product_id );
}
}
}
// Fire before the WC_Form_Handler::add_to_cart_action callback.
add_action( 'wp_loaded', 'woocommerce_maybe_add_multiple_products_to_cart', 15 );
/**
* Invoke class private method
*
* #since 0.1.0
*
* #param string $class_name
* #param string $methodName
*
* #return mixed
*/
function woo_hack_invoke_private_method( $class_name, $methodName ) {
if ( version_compare( phpversion(), '5.3', '<' ) ) {
throw new Exception( 'PHP version does not support ReflectionClass::setAccessible()', __LINE__ );
}
$args = func_get_args();
unset( $args[0], $args[1] );
$reflection = new ReflectionClass( $class_name );
$method = $reflection->getMethod( $methodName );
$method->setAccessible( true );
$args = array_merge( array( $class_name ), $args );
return call_user_func_array( array( $method, 'invoke' ), $args );
}
It works just like you'd expect, by providing a comma separated list of products. It even works with quantities using ?add-to-cart=63833:2,221916:4
I was, and am still looking for a 'pure' solution that allows to add multiple products to the cart without having to install a plugin or add custom actions. But for many, the above might be an appropriate solution

Woocommerce sortable columns not working

I've added a couple of custom field columns to our Woocommerce orders list in the admin of WordPress using the methods below, but the sort is not working....
add_filter( 'manage_edit-shop_order_columns', 'my_wc_columns' );
function my_wc_columns($columns){
$new_columns = (is_array($columns)) ? $columns : array();
unset( $new_columns['order_actions'] );
$new_columns['program_id'] = 'Program';
$new_columns['constituent_id'] = 'Constituent ID';
$new_columns['order_actions'] = $columns['order_actions'];
return $new_columns;
}
add_action( 'manage_shop_order_posts_custom_column', 'my_wc_column_values', 2 );
function my_wc_column_values($column){
global $post;
if ( $column == 'program_id' ) {
$program = get_post_meta( $post->ID, '_program_id', true );
$program_title = get_the_title($program);
$column_val = (isset($program) && $program>0 ? $program_title : 'All');
echo '<span>' . my_programs_get_name( $column_val ) . ' (' . $program . ')</span>';
}
if ( $column == 'constituent_id' ) {
$consid = get_post_meta( $post->ID, 'constituent_id', true );
$column_val = (isset($consid) && $consid != "") ? $consid : "";
echo '<span>' . $column_val . '</span>';
}
}
// Make column sortable
add_filter( "manage_edit-shop_order_sortable_columns", 'my_wc_column_sort' );
function my_wc_column_sort( $columns ) {
$custom = array(
'program_id' => '_program_id',
'constituent_id' => 'constituent_id',
);
return wp_parse_args( $custom, $columns );
}
I expected to have an issue perhaps with the program name, since it is an id that needs to be translated via a custom function to a name, but neither column is sorting properly. The records change order after clicking their column titles, but I cannot tell how the sort is being done. The program is not sorting on name or ID and both are seem random but consistent. Keep in mind both fields are custom fields that may or may not have a value defined. How can I make this sortable?
Here's a good tutorial on custom sortable columns. After you register the column, you need to handle the actual sorting. Sadly, that part doesn't happen automagically. Untested, but adapted from the above tutorial:
add_action( 'pre_get_posts', 'manage_wp_posts_be_qe_pre_get_posts', 1 );
function manage_wp_posts_be_qe_pre_get_posts( $query ) {
/**
* We only want our code to run in the main WP query
* AND if an orderby query variable is designated.
*/
if ( $query->is_main_query() && ( $orderby = $query->get( 'orderby' ) ) ) {
switch( $orderby ) {
// If we're ordering by 'program_id'
case 'program_id':
// set our query's meta_key, which is used for custom fields
$query->set( 'meta_key', '_program_id' );
/**
* Tell the query to order by our custom field/meta_key's
* value
*
* If your meta value are numbers, change 'meta_value'
* to 'meta_value_num'.
*/
$query->set( 'orderby', 'meta_value' );
break;
}
}
}

WooCommerce filter products by range

For example I have two variations, 'from_year' and 'until_year', how can I filtering products by range in product archive page.
If to call this url, will get only products that equals to the variables, but need from_year >= 2005 and until_year <= 20015
mysite.com/?from_year=2005&until_year=2015&post_type=product
you need date queries. Here's a good tutorial on date queries and here's the Codex on date parameters for WP_Query.
You will also need to filter query_vars to add your new query variables and pre_get_posts to modify the posts retrieved based on your new query vars.
With all that reference material, here's my attempt at how this should be done.
// Add query vars for filtering by years
function so_33714675_add_vars($query_vars) {
$query_vars[] = 'from_year';
$query_vars[] = 'until_year';
return $query_vars;
}
add_filter( 'query_vars', 'so_33714675_add_vars' );
// Filter product arvhice if query vars are present
function so_33714675_pre_get_posts($query) {
if ( !is_admin() && is_shop() && $query->is_main_query() ) {
$from_year = get_query_var('from_year');
$until_year = get_query_var('until_year');
$date_query = array();
if( ! empty( $from_year ) ){
$date_query['after'] = array(
'year' => $from_year,
);
}
if( ! empty( $until_year ) ){
$date_query['before'] = array(
'year' => $until_year,
);
}
// order by date
if( ! empty( $date_query ) ){
set_query_var( 'orderby', 'date' );
set_query_var( 'order', 'ASC' );
$date_query['inclusive'] = true;
set_query_var( 'date_query', array( $date_query ) );
}
}
}
add_action( 'pre_get_posts', 'so_33714675_pre_get_posts' );
Keep in mind that this is not fully tested. I'm 99% positive this is the correct approach, but it may need some tweaking.

Resources