How to hide wordpress empty SUB-CATEGORIS ONLY? - php

I am using this code to hide the empty categories, and it works.
add_filter( 'wp_get_nav_menu_items', 'nav_remove_empty_category_menu_item', 10, 3 );
function nav_remove_empty_category_menu_item ( $items, $menu, $args ) {
global $wpdb;
$nopost = $wpdb->get_col( "SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE count = 0" );
foreach ( $items as $key => $item ) {
if ( ( 'taxonomy' == $item->type ) && ( in_array( $item->object_id, $nopost ) ) ) {
unset( $items[$key] );
}
}
return $items;
}
But I want it to work for subcategories only... not the parent category.
So right now it hides some parent categories and I don't want that. I want to hide only the empty subcategories.
How can I do that?

You don't need a recursive function at all. The term_taxonomy table has another column called parent, which is set to 0 if it's a top level item, and will be the parent's ID if it's a child at any level. Simply add
AND parent != 0
to your query, and that should take care of it. (That said, I feel like pulling the terms and looping through the items for a match to unset is a bit overkill, but without knowing more it's tough to recommend a better solution, and this isn't a bad way to go about it - one of those "hammers to kill a fly" things.)
It may be (quite) a bit pedantic, but I also prefer writing SQL statements outside of the helper functions to make it easier to see at a glance what needs to be run through ::prepare, and deal with longer queries easier
add_filter( 'wp_get_nav_menu_items', 'nav_remove_empty_category_menu_item', 10, 3 );
function nav_remove_empty_category_menu_item( $items, $menu, $args ){
global $wpdb;
$sql = "
SELECT term_taxonomy_id
FROM $wpdb->term_taxonomy
WHERE count = 0
AND parent != 0
";
$no_posts = $wpdb->get_col( $sql );
foreach( $items as $key => $item ){
if( 'taxonomy' == $item->type && in_array($item->object_id, $no_posts) ){
unset( $items[$key] );
}
}
return $items;
}

Related

Best performance for mySQL and PHP: Lots of small queries, or a large one?

So i have this technicall dilemma:
I have a Wordpress site, with more than 50 categories (and rising). I want to get just the six categories with the newest article in them, and -if i get this right- i can't do it with a single typical WP Query, i'll have to improvise:
a) get from the database the last 200-300 posts, and by elimination in PHP filter by category, and show the six last updated categories, or
b) get from the database all my categories, and for each category ask again the database the last article by date - and show the six last updated categories.
i know that the first solution is not very safe, and will strech a little my server calculating with PHP, but the second will have more than 50+1 queries (and rising).
Considering that the site i'm working on has a couple of million visitors each month, which one do you think will give, from a technical point of view, the less stress to my servers?
Or i'm missing an more obvius solution?
Thanks for your time! :)
Every time you post something, you could save the taxonomy's name/slug/id in a transient on publishing/updating through set_transient() and save_post action hook.
Saving the data as an array and slicing it once it reach 6... array_search before updating...
To display them you would simply loop through the transient's value and get_taxonomy() or build a custom WP_Query looping through each of them.
Which would avoid you a lot of queries. Pretty much none...
here is an untested demo:
<?php
//Retieve the transient through get_transient( '_transient_wpso_73900689' );
add_action( 'save_post', function ( $post_ID, $post, $update ) {
$transient = '_transient_wpso_73900689';
$expiration = 0;
$prev_value = ! empty( get_transient( $transient ) ) ? get_transient( $transient ) : array();
$next_value = get_post_taxonomies( $post_ID );
$buffer = array();
if ( ! empty( $next_value ) ) {
foreach ( $next_value as $taxonomy ) {
//Search the previous transient array.
if ( array_search( $taxonomy, $prev_value ) === false ) {
//If the value doesn't exist, push the value to the array.
array_push( $buffer, $taxonomy );
};
};
$value = array_merge( $prev_value, $buffer );
//Reverse the array.
$value = array_reverse( $value );
//Retrieve the last 6.
$value = array_slice( $value, 0, 6 );
//Set the transient.
set_transient( $transient, $value, 0 );
};
}, 10, 3 );
Can,t you just join the articles and sort by the joined table ordered by date desc?
Something like:
Select from categories c
Left join articles a on a.category = c.id
Order by a.date desc group by c.id limit 6

Filtering the WooCommerce $order items by Category (term)

In this problem, I have all the pieces but I just can't seem to get them to fit together.
I have a printing template for WooCommerce orders that lists the products in the normal way, the order they are stored in the array which in turn is the order they were placed in the basket etc. However, we want them grouped by Category (Term). So this will mean that all items under term_id 17 are listed first, then all term_id 18, etc.
Ideally, this would be an automatic process where the code reads all of the Terms in use, then steps through them one at a time and outputs any products for that Term that are in the basket. But I'm nowhere near that stage yet.
So far I have had partial success with this code:
foreach ($order->get_items() as $item) {
$product_id = $item['product_id'];
$meta = $item['item_meta'];
$meta = array_filter($meta, function ($key) {
return !in_array($key, Order::getHiddenKeys());
}, ARRAY_FILTER_USE_KEY);
$terms = get_the_terms ( $product_id, 'product_cat' );
foreach ( $terms as $term ) {
$cat_id = $term->term_id;
if($cat_id === 18) {
var_dump($item['name']);
}
}
This will successfully dump the order items that are categorised in term_id 18. However, if I modify the IF statement like this:
if($cat_id === 18) {
var_dump($item['name']);
} elseif($cat_id === 17) {
var_dump($item['name']);
}
I would have expected it to output the term_id 18 items, then the term_id 17 ones AFTER them. Unfortunately, it simply shows the array in the default order of 17, then 18, despite the fact the code is laid out in this fashion.
I thought that maybe it was outputting in this way because it's not modifying the original array, simply masking parts of it at different points. So, I have been experimenting with the array_filter function, but I can't seem the get the logic right. I know this attempt is awful, but this is as far as I've gotten experimenting with array_filter:
function test ($var) {
foreach ( $terms as $term ) {
$cat_id = $term->term_id;
if($cat_id === 18) {
print_r($item['name']);
}
}
return $var;
}
foreach ($order->get_items() as $item) {
$product_id = $item['product_id'];
$meta = $item['item_meta'];
$meta = array_filter($meta, function ($key) {
return !in_array($key, Order::getHiddenKeys());
}, ARRAY_FILTER_USE_KEY);
$terms = get_the_terms ( $product_id, 'product_cat' );
print_r(array_filter($terms, "test"));
I know it's significantly wrong, but I just can't get my head around it. I'm fine with basic PHP and WordPress PHP usage normally, but WooCommerce is so much more complicated!
Just to recap, I want the products in the order printed / echoed / etc in Category Order (ideally automatically). I don't really mind how this is achieved so long as it's safe and secure obviously.
It looks to me like you're looping the orders, but you're not looping the categories you want to be ordering them.
I've not run this code, but the pattern should do what you want I think.
// Group items into Categories - If you have a custom order you wish, pre-populate this array, or you can sort after
$categories = [];
foreach ($order->get_items() as $item) {
$product_id = $item['product_id'];
$terms = get_the_terms($product_id, 'product_cat');
$cat_id = $terms[0]->term_id;
$categories[$cat_id][] = $item;
}
// Loop Categories
foreach($categories as $category => $items){
echo sprintf("<h1>%s</h1>", $category);
//Loop Items in Category
foreach ($items as $item) {
print_r($item['name']);
}
}

Search by order item SKU or ID in WooCommerce Orders Admin page

What I am trying to do is to be able to search by order item SKU or ID in the WooCommerce Orders Admin page.
What I have found/done till now, but with no success is the following at functions.php file.
add_filter( 'woocommerce_shop_order_search_fields', 'woocommerce_shop_order_search_sku' );
function woocommerce_shop_order_search_sku( $search_fields ) {
$args = array( 'post_type' => 'shop_order' );
$orders = new WP_Query( $args );
if ( $orders->have_posts() ) {
while( $orders->have_posts() ) {
$post = $orders->the_post();
$order_id = get_the_ID();
$order = new WC_Order( $order_id );
$items = $order->get_items();
foreach( $items as $item ) {
$search_order_item_sku = wp_get_post_terms( $item['product_id'], 'search_sku' );
foreach( $search_order_item_sku as $search_sku ) {
add_post_meta( $order_id, "_search_sku", $search_sku->sku );
}
}
}
};
$search_fields[] = '_search_sku';
return $search_fields;
}
I suppose the issue is the value of $search_sku at the line with the add_post_meta.
I have also tried it with get_sku(), $item['sku'] with no luck.
You have the right idea about saving extra metadata to the order. As jbby and helgatheviking suggest, there is no built-in postmeta for product_id or sku available by default in the woocommerce orders api. Your methodology for accessing and saving the metadata wasn't quite right, however. wp_get_post_terms will access custom taxonomy information, not metadata (use get_post_meta for that). You will be able to do what you were trying to do with this filter:
add_filter( 'woocommerce_shop_order_search_fields', function ($search_fields ) {
$posts = get_posts(array('post_type' => 'shop_order'));
foreach ($posts as $post) {
$order_id = $post->ID;
$order = new WC_Order($order_id);
$items = $order->get_items();
foreach($items as $item) {
$product_id = $item['product_id'];
$search_sku = get_post_meta($product_id, "_sku", true);
add_post_meta($order_id, "_product_sku", $search_sku);
add_post_meta($order_id, "_product_id", $product_id);
}
}
return array_merge($search_fields, array('_product_sku', '_product_id'));
});
Strictly speaking you should probably move the calls to add_post_meta into a hook that runs when the order is originally saved to the database--this will prevent unnecessary legwork whenever you search through order.
#blacksquare, #jibby, #helgatheviking you are the men! This is the code that works, due to your help.
//Search by product SKU in Admin Woocommerce Orders
add_filter( 'woocommerce_shop_order_search_fields', function ($search_fields ) {
$posts = get_posts(array('post_type' => 'shop_order'));
foreach ($posts as $post) {
$order_id = $post->ID;
$order = new WC_Order($order_id);
$items = $order->get_items();
foreach($items as $item) {
$product_id = $item['product_id'];
$search_sku = get_post_meta($product_id, "_sku", true);
add_post_meta($order_id, "_product_sku", $search_sku);
}
}
return array_merge($search_fields, array('_product_sku'));
});
While #Nikos and #blacksquare 's answers work, new post metas are added to every order on every search. If you have 100 orders and make 10 searches, there will be at least 100*10 = 1000 _product_sku entries in the wp_postmeta table. If some orders contain multiple products, there will be even more.
As #blacksquare suggested, add_post_meta should be called when the order is saved. That said, if the site is small and backend search performance isn't too much of a concern, the following code would work without creating redundant _product_sku entries.
add_filter( 'woocommerce_shop_order_search_fields', 'my_shop_order_search_fields') );
public function my_shop_order_search_fields( $search_fields ) {
$orders = get_posts( array(
'post_type' => 'shop_order',
'post_status' => wc_get_order_statuses(), //get all available order statuses in an array
'posts_per_page' => 999999, // query all orders
'meta_query' => array(
array(
'key' => '_product_sku',
'compare' => 'NOT EXISTS'
)
) // only query orders without '_product_sku' postmeta
) );
foreach ($orders as $order) {
$order_id = $order->ID;
$wc_order = new WC_Order($order_id);
$items = $wc_order->get_items();
foreach($items as $item) {
$product_id = $item['product_id'];
$search_sku = get_post_meta($product_id, '_sku', true);
add_post_meta( $order_id, '_product_sku', $search_sku );
}
}
return array_merge($search_fields, array('_product_sku')); // make '_product_sku' one of the meta keys we are going to search for.
}
While a better solution might be calling add_post_meta when an order is created, extra efforts are needed to create _product_sku for existing orders, and you have to create the _product_sku for orders made while the code isn't activated. For simplicity sake, I'd just use the solution suggested above.
p.s. #Nikos 's solution does have one (debatable) advantage - if you change a product's SKU after orders are made, Nikos's solution will find those orders using the new SKU, while the solution above will not. That said, a product's SKU should NOT be changed anyway, and it's debatable whether searching new SKUs should show old orders.
You may also want to look into https://uk.wordpress.org/plugins/woo-search-order-by-sku/ which does not overloads post meta, but alters the where clause.
As stated in answer by Ming Yeung, any solution which uses the add_post_meta() function will increase your database size, which might not be ideal for larger stores. That sort of approach will also potentially make the searching of Woocommerce orders in admin quite slow.
An alternative solution is to make use of one of the existing "index" post meta records that Woocommerce uses, and append your data to those records (but only append once).
The following code does exactly that. It does not add SKU (as asked in question) but instead allows for Woocommerce order search to include Country name (as opposed to country code). However this code could be easily adapted to also include SKU if needed:
// when doing a search within Woocommerce orders screen, include the Country name as part of the search criteria
// this code has been optimised to do as little DB work as possible (looks for any index values which haven't been updated yet, and updates them)
// this code also makes use of existing post meta "index" records, instead of creating additional new post meta rows (which would fill up database unnecessarily)
add_filter('woocommerce_shop_order_search_fields', function($search_fields) {
global $wpdb;
$records_to_update = $wpdb->get_results("SELECT * FROM $wpdb->postmeta WHERE (meta_key LIKE '%_address_index%') AND (meta_value NOT LIKE '%[HAS_COUNTRY]%')");
foreach ($records_to_update as $record) {
$order = new WC_Order($record->post_id);
if ($record->meta_key == '_billing_address_index') {
$country_name = WC()->countries->countries[ $order->get_billing_country() ];
} else {
$country_name = WC()->countries->countries[ $order->get_shipping_country() ];
}
update_post_meta($record->post_id, $record->meta_key, $record->meta_value . ' ' . $country_name . ' [HAS_COUNTRY]');
}
return $search_fields;
});

How can I sort posts by featured image(no image posts at the bottom) in wordpress using PHP?

I am working with the Wordpress Query Object using the WP Types/Views Toolset.
http://wp-types.com
We built out the parametric search which allows the user to search through posts using various taxonomies. It works fine, it display search results as needed. BUT..
Most of the posts don't have featured images, we want to display posts with featured images at the top, and the featured imageless posts would go below.
I have a good head start on this as far as logic goes, just need a bit of help.
The code below allows me manipulate the query before rendering it to the user.
add_filter( 'wpv_filter_query_post_process', 'prefix_rearrange_by_thumbnail', 10, 3 );
function prefix_rearrange_by_thumbnail( $query, $view_settings, $view_id ) {
// sort posts by thumbnail
return $query;
}
How can I sort through the query->posts and rearrange them so the ones that do have featured images show up before those without.
All of the WP_Post objects are found in an array under $query->posts. You can sort the array using usort() based on whatever criteria you want. Keep in mind that this will only sort each page of results as that is what is returned.
add_filter( 'wpv_filter_query_post_process', 'prefix_rearrange_by_thumbnail' );
function prefix_rearrange_by_thumbnail( $query ) {
$posts = $query->posts;
usort( $posts, 'sort_by_thumbnail' );
return $query;
}
// function used to sort the posts array
function sort_by_thumbnail( $a, $b ){
// the the featured image id so we can sort by it
$a_thumb = get_post_meta( $a->ID, '_thumbnail_id', true );
$b_thumb = get_post_meta( $b->ID, '_thumbnail_id', true );
if ( empty( $a_thumb ) && ! empty( $b_thumb ) ) return 1;
if ( ! empty( $a_thumb ) && empty( $b_thumb ) ) return -1;
return 0;
}
To sort all the pages of the results you need to modify the arguments that are passed to WP_Query using the wpv_filter_query filter. There is a meta_key called "_thumbnail_id" set for each post/page that has a featured image, but the issue is that this meta_key is not set at all for posts/pages without a featured image, so if you use it the results will be sorted by the ID of the featured images, but if it is missing they will be omitted. One option is to set another flag based on that meta_key to use in your sort. A one time cleaned would be needed then you could use a hook on save_post
One time SQL (adjust for your db prefix):
INSERT INTO wp_postmeta( post_id, meta_key, meta_value )
SELECT p.ID, '_has_featured_image', IF ( meta_value IS NULL, 0, 1 ) AS _has_featured_image
FROM wp_posts p
LEFT JOIN wp_postmeta m ON p.ID = m.post_id AND meta_key = '_thumbnail_id'
WHERE p.post_status = 'publish'
AND p.post_type IN ('post','page')
Hook on save_post to keep "_has_featured_image" clean going forward:
add_action( 'save_post', 'set_has_featured_image' );
function set_has_featured_image( $post_id ){
$post = get_post( $post_id );
switch ( $post->post_type ){
case 'post': case 'page':
$thumbnail_id = get_post_meta( $post_id, '_thumbnail_id', true );
update_post_meta( $post_id, '_has_featured_image', ! empty( $thumbnail_id ) );
break;
}
}
Now you can filter on the "_has_featured_image" meta_key:
add_filter( 'wpv_filter_query', 'prefix_rearrange_by_thumbnail' );
function prefix_rearrange_by_thumbnail( $args ){
$args['orderby'] = 'meta_value';
$args['meta_key'] = '_has_featured_image';
return $args;
}
I managed to do it in an easier way...
add_filter( 'wpv_filter_query_post_process', 'prefix_rearrange_by_thumbnail', 10, 3 );
function prefix_rearrange_by_thumbnail( $query, $view_settings, $view_id ) {
if ( !empty( $query->posts ) ) {
$sorted_posts_top = array();
$sorted_posts_bottom = array();
$all_posts = $query->posts;
foreach ($all_posts as $post) {
if ( has_post_thumbnail($post->ID) ) {
$sorted_posts_top[] = $post;
}
else {
$sorted_posts_bottom[] = $post;
}
$query->posts = array_merge((array)$sorted_posts_top, (array)$sorted_posts_bottom);
}
}
return $query;
}
However, your answer is still VERY helpful. Thank you SO MUCH for sharing!!!!!!!!!!!!!!!!

MySQL JOIN or UNION with and based on previous resultset, WordPress pre_get_posts and post_meta

Trying to recursively select database fields based on parent IDs of current selection in a WordPress environment.
On the WordPress side:
I'm doing a pre_get_posts filter to retrieve only the posts associated with a user meta. This was simple, until I realized that these posts could have parents and that would break the interface. Answers involving fixing the WP_List_Table sorting algorithm to allow children to be re-sorted hierarchically may be accepted. Without including parents, the table looks like this:
The meta query looks like this:
// pre_get_posts
$meta_query = $query->get('meta_query');
$meta_query[] = array(
'key' => '_my_meta_key',
'value' => get_user_meta( $user_id, '_my_meta_key' ),
);
$query->set('meta_query', $meta_query );
On the MySQL side, adding the parents to this query would work just as well. As it stands, the query above works like the first select clause of the SQL fiddle here:
SQL Fiddle
A) In my limited SQL knowledge, I haven't figured out how to use the first query's id column as a parameter. Here, I've copy and pasted the entire thing to use as an IN parameter, which runs the query twice. Is there a way to alias the first query to use the id field in the second query?
B) How can I also include the parents of the parents (and so on) returned in the second SELECT statement?
I ended up manually adding the parents back into the result after wp_query is parsed. So far, I haven't run into any issues. It's not exactly what I wanted, as it produces extra (not too many in this case) and thought there might be a more efficient way, but it is what it is. I'll leave this open a while in case anyone comes up with anything better.
Note that since the user cannot edit these posts, they are not editable (awesome!)
Example code:
// users must see parent pages on the Edit screen
// even when they can't edit them
// to preserve hierarchy
add_action( 'the_posts', 'add_post_parents_not_in_meta_query'), 10, 2 );
function add_post_parents_not_in_meta_query( $posts, $query ) {
// only on pages with screen
if ( ! function_exists( 'get_current_screen' ) )
return $posts;
$screen = get_current_screen();
// only on edit screen
if ( $screen->base !== 'edit')
return $posts;
// only during main query
if ( ! $query->is_main_query() )
return;
// get an array of found IDs
$post_ids = wp_list_pluck( $posts, 'ID' );
foreach( $posts as $post ) {
// check to see if exists, and if we already got this post
while ( ! empty( $post->post_parent ) && ! in_array( $post->post_parent, $post_ids ) ) {
$post = get_post( $post->post_parent );
if ( ! empty( $post ) ) {
$posts[] = $post;
$post_ids[] = $post->ID;
}
}
}
return $posts;
}
Result:

Categories