Attempting to speed up WooCommerce Subscriptions admin

If you use WooCommerce along with their WooCommerce Subscriptions plugin you will probably notice the site becomes slow after a number of subscribers. As I wrote in a previous article, there are some tricks that may make it faster.

In this article I will present some code which might help with speeding up some pages which use lots of queries.

User’s active subscriptions count

If you ever used the Query Monitor plugin you probably noticed that on the Users list, even when loading 20 users WordPress does many queries ( hundreds ). With WooCommerce Subscriptions enabled there is one query which is especially resource expensive. That’s the query used to populate the “Active subscriber” column. You can go ahead and remove that if you don’t use it because you can also filter the users by the “subscriber” role. So here’s the code to do that:

// remove the subscription column in users table
remove_filter( 'manage_users_columns', 'WC_Subscriptions_Admin::add_user_columns', 11 );
remove_filter( 'manage_users_custom_column', 'WC_Subscriptions_Admin::user_column_values', 11 );

Comments count

Another very expensive query which I found might slow your website down if you have lots of WooCommerce orders ( which when using subscriptions is highly probable ) is the comments count. As you probably know the WooCommerce order notes are stored as comments, on average you have at least 2 notes for each order which results in a lot of comments.

You can prevent that count from taking place in the admin using the code below:

if ( is_admin() ) {
    add_filter( 'wp_count_comments', function( $counts, $post_id ) {
        if ( $post_id )
            return $counts;
        return (object) array(
            'approved'        => 0,
            'spam'            => 0,
            'trash'           => 0,
            'total_comments'  => 0,
            'moderated'       => 0,
            'post-trashed'    => 0,
        );
    }, 10, 2 );
}
// If running WooCommerce, remove their filter so that nothing funky goes down
remove_filter( 'wp_count_comments', array( 'WC_Comments', 'wp_count_comments' ), 10 );

Another quick improvement if you have lots of “processing” orders is to remove the count that shows up in the admin menu ( which is being called on every screen obviously ).

// Remove order count from admin menu
add_filter( 'woocommerce_include_processing_order_count_in_menu', '__return_false' );

Changing pseudo-elements style with JavaScript

Recently I had to add a color-picker that changed the background-color of a div and I was faced with the problem of changing a pseudo-element’s style. The div I was manipulating with JS had a :before styled to give a tilted effect, so the top of the element looked like this:

pseudo-element

If you didn’t know, styling a pseudo-element ( like :after or :before ) is quite complicated in JavaScript. You have to insert css rules directly in the page’s stylesheets and that doesn’t work ok in all browsers.

I found that the easiest way to change the background color of pseudo-elements is something very simple, instead of using:

.tilted-div:before {
    background-color: #008000;
}

You can use:

.tilted-div {
    background-color: #008000;
}
.tilted-div:before {
    background-color: inherit;
}

So now when you change the background-color using style attributes it will reflect on the :before div.

This might seem a bit obvious – but it reminds you that the inherit property can be used in a variety of situations.

Other uses

Another example in which I found this useful was with a custom bullet icon in a list. Let’s say you have this code:

.bullet-list-element {
    color: #33A880;
}
.bullet-list-element:before {
    content: "◎";
    color: inherit;
}

Now if you change the color of the text in the “bullet-list-element” it will reflect on the custom bullet added before it, which is useful especially when previewing color-changing feature something like a premium theme.

Gravity Forms disabled inputs that submit correctly

If you looked for how to make a Gravity Forms field disabled/read-only you probably found the official article from their documentation. They suggest using a piece of JavaScript added to the “gform_pre_render” action, it looks like this:

add_filter('gform_pre_render_1', 'add_readonly_script');
function add_readonly_script($form){
    ?>
    <script type="text/javascript">
        jQuery(document).ready(function(){
            jQuery("li.gf_readonly input").attr("readonly","readonly");
        });
    </script>
    <?php
    return $form;
}

That suggestion is good if you work with simple text inputs ( or swapping that input to textarea ) but for select/drop-down it’s a bit more complicated, especially the ones that use the Chosen JavaScript library.

Including all form elements

I wanted to include all form elements so I used the “:input” jQuery selector which includes all form elements.

Next I wanted to also disable select elements that use the Chosen library – so I added trigger, so the call looks like this:

jQuery(function($){
    jQuery('.mgf_disabled :input').prop('disabled', true).trigger("chosen:updated");
});

The looks

You probably noticed I made the fields disabled instead of “read-only” and that’s because by default the browsers don’t display the fields differently when they are marked as “read-only”, and I didn’t want to add any custom css. You can just skip this part and use read-only if you add some styles to that using the :read-only selector:

input:-moz-read-only { /* For Firefox */
    background-color: yellow;
}
input:read-only {
    background-color: yellow;
}

If you decide to use disabled – you probably know that those fields get ignored when submitting the form so the solution is to remove that attribute on the submit action. The final code looks like this:

jQuery(function($){
    jQuery('.mgf_disabled :input').prop('disabled', true).trigger("chosen:updated");
    jQuery('.gform_wrapper form').submit(function() {
        jQuery('.mgf_disabled :input').prop('disabled', false);
    });
});

 

AWS S3 preview private and encrypted files with PHP

Recently I had to develop a WordPress plugin that is used to manage files stored on Amazon AWS S3. Due to the nature of the files which contain sensible information they are stored as private and using the AES256 ServerSideEncryption.

In order to serve the files to authorized users an AJAX call is used to generate a temporary link from the AWS PHP SDK. My problem is that for these links the “content-disposition” header is set to “attachment” which forces a download. As the client wanted to offer the option to also view directly in the browser these ( PDF ) files, I was faced with a problem.

Previewing the files without forcing a client download

The Amazon AWS documentation specifies an optional content-disposition parameter. So, for example, in a “getObjectUrl” call you would use:

$args = array(
    'ResponseContentDisposition' => 'attachment; filename=testing.pdf'
);
$request = $s3Client->getObjectUrl( $bucket, 'example/test.pdf', '+2 minutes', $args );

Those arguments get passed to the “getObject” function as explained here. The problem is that for some reason this parameter is ignored – probably because of the encryption.

I found a quick solution to this problem by downloading the file and serving directly from PHP on my server instead of trying to change the content-disposition on Amazon’s requests.

So in the WordPress plugin I added a simple function which takes a file and serves it with content-disposition inline:

add_action( 'wp_ajax_m_view_link', array( $this, 'view_pdf' ) );
function view_pdf() {
   if ( isset($_GET['url']) && is_user_logged_in() ) {
      header('Content-disposition: inline; filename='.basename($_GET['url']));
      header("Content-type:application/pdf");
      echo file_get_contents($_GET['url']);
   }
   die();
}

So now when calling /wp-admin/admin-ajax.php?action=m_view_link&url=url_of_amazon_file the users can preview the files directly in their browsers.

Notes:

To prevent potential security threats you should add some checks to the “view_pdf” function above.

Obviously you can use this for other purposes than Amazon S3 files it’s a quick way to change the content-disposition of certain external files.

Common WordPress caveats

This is an article which I will edit in the future with other common WordPress caveats so feel free to send suggestions.

Child_of vs parent in get_terms

Although this makes sense immediately after you read the documentation I ran into this issue. When getting custom taxonomy terms with the “get_terms” function you can use as argument either “child_of” or “parent”. The main difference between the two being that if the taxonomy is hierarchical ( like “Categories” ) using “parent” you will only get first descendants of that term. Using “child_of” you will get all the children and this can cause duplicate results. This is most likely to happen in a “related terms” situation.

tax_query array of arrays

It might be obvious to everybody by now but if you ever go crazy why tax_query or meta_query in a WP_Query isn’t working it could be that the format is wrong. So be careful to use an array of arrays, like this:

$args = array(
    'meta_query' => array(
        array(
            'key' => 'color',
            'value' => 'red'
        )
    )
);

Multisite max_upload_file_size

Creating a WordPress multi-site network you might encounter an issue with file uploads after the setup. Before going crazy changing all the php.ini settings and wp-config.php with no result make sure you check the value of “max_upload_file_size” in the network settings page.

That’s /wp-admin/network/settings.php in case you don’t know where to find it. Also note that it’s a value in kb – so don’t set it to something like 64.

WooCommerce subscriptions speed up from external plugin

If you are running a WooCommerce subscriptions website you might avoid front-end delays by using proper caching. But in the admin you will still encounter issues with large databases and not only.

Using the Query monitor plugin I found that the class used by WooCommerce subscriptions to check for deprecated hooks is actually doubling the loading times for some of the pages. The class “WCS_Dynamic_Hook_Deprecator” is adding an action on the “all” hook. The “all” hook should never be used unless really needed ( this is a real case where this is needed ).

Removing the action

The main problem when trying to remove the action is that it is adding using an anonymous function in a class instance:

add_filter( 'all', array( &$this, 'check_for_deprecated_hooks' ) );

As explained in this awesome WordPress Stackexchange answer, because the function is anonymous WP generates a name for it using the _wp_filter_build_unique_id function. This means we can find the hook and remove it using the $GLOBALS variable.

It’s much easier than it sounds when using the function below ( from the same answer mentioned above ):

if ( ! function_exists( 'remove_anonymous_object_filter' ) )
{
   /**
    * Remove an anonymous object filter when a class uses $this
    *
    * @param  string $tag    Hook name.
    * @param  string $class  Class name
    * @param  string $method Method name
    * @return void
    */
   function remove_anonymous_object_filter( $tag, $class, $method )
   {
      $filters = $GLOBALS['wp_filter'][ $tag ];
      if ( empty ( $filters ) )
      {
         return;
      }
      foreach ( $filters as $priority => $filter )
      {
         foreach ( $filter as $identifier => $function )
         {
            if ( is_array( $function)
                 and is_a( $function['function'][0], $class )
                     and $method === $function['function'][1]
            )
            {
               remove_filter(
                  $tag,
                  array ( $function['function'][0], $method ),
                  $priority
               );
            }
         }
      }
   }
}

Add this as a stand-alone plugin or in your theme’s “functions.php” file. Afterwards you can remove the WooCommerce Subscriptions hook using:

// remove the hook deprecator check to speed up woocommerce subscriptions
remove_anonymous_object_filter('all', 'WCS_Dynamic_Hook_Deprecator', 'check_for_deprecated_hooks');

Obviously you can use this function for any other situation in which a class assigns an anonymous function to a hook.

Allow WordPress multi-site in sub-directories for existing websites

When trying to enable the WordPress multi-site you might get the following message in the setup:

“Because your install is not new, the sites in your WordPress network must use sub-domains. The main site in a sub-directory install will need to use a modified permalink structure, potentially breaking existing links.”

What this means is that there might be a conflict between existing pages/posts and new sites. So if you previously had a page with the slug “mobile” and after the setup you create a sub-site called “mobile” well…you see the problem.

The easiest way to avoid that restriction ( keeping in mind to avoid the slug conflict above ) is by adding the code below to an active plugin or theme.

add_filter( 'allow_subdirectory_install',
   create_function( '', 'return true;' )
);

After that, you can run the setup as for a new website, when you’re done don’t forget to remove the code.

WordPress admin check if content changed using Heartbeat

When creating a WordPress admin panel it is common to want to refresh the page based on real-time content changes. The most straightforward way of doing that is by hooking into the WP Heartbeat API.

Using the Heartbeat API you can have almost real-time updates to the admin content.

What is the WordPress Heartbeat API?

It was introduced in WordPress version 3.6 and basically it does a regular Ajax call. It does so in order to check if the user is still logged in or if someone else is editing a post. This way it can show you the whole “someone else is editing this post” screen which you can “Take over”.

The Ajax call is, by default, being triggered based on user activity and at a certain interval but we can also change that to a more “stable” call at a given interval.

But first, the data update!

When you are ready to update the data in your admin panel ( or another plugin’s admin panel ) there are 2 steps to follow:

  1. You add a piece of JavaScript that adds your custom variable
  2. You filter the value of “heartbeat_received” and populate the data which will also be used in the script you added above.

Here’s an example of doing that:

add_filter( 'heartbeat_received', 'm_add_heartbeat_data'), 10, 2 );
function m_add_heartbeat_data( $response, $data ) {
   if ( isset($data['daed_version']) && $data['daed_version']=='dashboard_editorial' ) {
      $response['daed_version'] = $this->get_version();
   }
   return $response;
}
add_action( 'admin_print_footer_scripts', 'm_heartbeat_footer_js');
function m_heartbeat_footer_js() {
   global $pagenow;
   // check the page you are on, maybe you don't want to run this on all the admin pages
   if( 'admin.php' != $pagenow )
      return;
   ?>
   <script>
      (function($){
         $(document).on('heartbeat-send', function(e, data) {
            // add your custom variable which will be populated in the ajax request
            data['m_sales'] = 'yes_send';
         });
         $(document).on( 'heartbeat-tick', function(e, data) {
            // don't run the script if the custom variable is not present
            if ( ! data['m_sales'] )
               return;
            if ( data['m_sales'] ) {
               $('#m_sales_count').text(data['m_sales']);
            }
         });
         // set the heartbeat interval - explained below
         wp.heartbeat.interval( 'slow' );
      }(jQuery));
   </script>
   <?php
}

If you’re asking yourself “why not add this in a separate js file?” – the answer is, because it will get cached and maybe you want to pass some variables to it.

Changing the interval

You can change the Heartbeat interval either by hooking into the “heartbeat_settings” filter using PHP and changing the “interval” value:

function m_heartbeat_settings( $settings ) {
   $settings['interval'] = 60; //only values between 15 and 60 seconds allowed
   return $settings;
}
add_filter( 'heartbeat_settings', 'm_heartbeat_settings' );

Or by using the JavaScript functions as I have done above using one of the 3 available values: fast, slow or standard.

// 5 seconds but no more than 2 and a half minutes
wp.heartbeat.interval( 'fast' );
// 60 seconds
wp.heartbeat.interval( 'slow' );
// 15 seconds
wp.heartbeat.interval( 'standard' );

 

Adding a custom table to the WordPress database

Many times in a WordPress plugin you want to store data in an easy accessible way without bloating the default WP tables. That’s when a custom database table comes in handy.

This isn’t something new just a way to do it clean and some warnings from troubles encountered in live environments.

The code

This is just a simple example of a database class which I include in custom plugins:

class M_Db {
    private static $db_version = '1.0.0';
    const DB_VERSION_NAME = 'm_db_version';
    const POSTS_TABLE = 'm_info';
    public static function install() {
        global $wpdb;
        $installed_ver = get_option(self::DB_VERSION_NAME);
        if ( $installed_ver != self::$db_version ) {
            $table_name = $wpdb->prefix . self::POSTS_TABLE;
            $charset_collate = $wpdb->get_charset_collate();
            $sql = "CREATE TABLE $table_name (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            user_id bigint(20) DEFAULT 0 NOT NULL,
            region VARCHAR(50) NOT NULL,
               UNIQUE KEY id (id)
           ) $charset_collate;";
            require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
            dbDelta( $sql );
            update_option( self::DB_VERSION_NAME, self::$db_version );
        }
    }
}

This code will allow you to create a custom table and also easily update its structure using the version check. After a change is made ( like adding a new column ) you just need to change the static version and the table will be updated.

Of course, you don’t want to run this code each time the website is loaded so you’ll have to call the “install” function from the proper hook.

In a plugin ( and hopefully you won’t be creating custom tables with a theme! ) you should run this on plugin activation by adding this code to the main plugin file:

register_activation_hook( __FILE__, array( 'M_Db', 'install' ) )

Warnings

  • if you have a column for user_id – make sure you match its format to the WordPress wp_user ID format. You wouldn’t want user ids to get saved as the maximum value of an integer for all the users with a larger id number – this applies for any relation column.
  • make sure you consider adding a case for plugin deactivation if that’s necessary using register_deactivation_hook.
  • on the same note above – if the plugin will be present in a repository, make sure you handle updates properly.

WooCommerce Ajax add to cart custom redirect

If you want to redirect to a custom url after adding a product to cart you can achieve that pretty easily if you don’t have the Ajax functionality enabled. An example of that can be seen here.

What if you use Ajax

Using Ajax that’s a little bit harder to achieve but you can do that using by “fake” throwing an error as that is being handled directly by WooCommerce.

In the WooCommerce add-to-cart.js file you will see this piece of code:

if ( response.error && response.product_url ) {
   window.location = response.product_url;
   return;
}

You can use this to manipulate the behavior by hooking into “woocommerce_ajax_added_to_cart”.

add_action('woocommerce_ajax_added_to_cart', 'm_custom_redirect');
function m_custom_redirect( $product_id ) {
    // add your check if certain product should trigger the redirect
    if ( $product_id == 34 ) {
        // custom redirect url
        $custom_redirect_url = get_permalink(55);
        $data = array(
            'error'       => true,
            'product_url' => $custom_redirect_url
        );
        wp_send_json( $data );
        exit;
    }
}

This can be used in a variety of situations in which you want to suggest another product after a product has been added to the cart.