How to Disable the Resubscribe feature in Woocommerce Subscriptions Plugin

This brief guide is aimed at developers familiar with WordPress who are looking to disable the Resubscribe feature in WooCommerce’s very own “Woocommerce Subscriptions” plugin.

Let’s dive right in.

Outline

  1. Solution(s).
  2. Show the Work.

At the time of this writing (2023-07-03), there is no option to turn off the “Resubscribe” functionality. If you’ve come into a scenario where that is becoming problematic (not unheard of – plus, I’m also having this issue), you might be searching for a solution.

…And probably didn’t find anything that helped.

Solution(s).

Well I dug through the code and found two quick solutions that you can drop in your child or custom theme’s functions.php file, a custom plugin, or a mu-plugin drop in. The first solution is my preferred method as it effectively disables the feature:

// This simply says "nobody has the authority to resubscribe a subscription ever"
add_filter( 'wcs_can_user_resubscribe_to_subscription', '__return_false' );

The alternative just removes the button from rendering, but if a customer knew how to mock up the url to do it, or was provided a link, could still purchase the resubscription. This might be a better solution for scenarios where you want to modify or tailor how customers get to “Resubscribe”, but still leave it as a functioning feature:

// This method simply removes action buttons like "reactiveate", "resubscribe", and "cancel" from being rendered.
add_filter( 'wcs_view_subscription_actions', function($subscription_actions){
    if ( !empty($subscription_actions['resubscribe']) ) {
        unset($subscription_actions['resubscribe']);
    }
    return $subscription_actions;
});

Both will remove the “Resubscribe” button from being rendered, but only the wcs_can_user_resubscribe_to_subscription filter denies the resubscribe functionality across the site.

Show the Work.

I’d like to show my work so you can A.) follow along, B.) be more informed in your decisions, and/or C.) trust the process coming to these solutions.

Let’s start in the Subscription Details template where the “Resubscribe” button gets rendered: ./woocommerce-subscriptions/vendor/woocommerce/subscriptions-core/templates/myaccount/subscription-details.php. Inside that file (currently line 78), there’s a function called wcs_get_all_user_actions_for_subscription() who’s job is to get all the “Actions” that are contextually available for that subscription to that user.

We then look at that function’s definition in ./woocommerce-subscriptions/vendor/woocommerce/subscriptions-core/includes/wcs-user-functions.php and see that upon return, it has the second solution’s filter. Here we can remove any of the available options from rendering as an available action. Mind you, this *only* removes the “Resubscribe” button from rendering in the “view subscription” template. Placing a similar button anywhere else will still work!

However, if we’re specifically looking at disabling the “Resubscribe” feature altogether, we see in the middle of the wcs_get_all_user_actions_for_subscription() function, it leverages a function called wcs_can_user_resubscribe_to() – that sound promising! Finding its definitiong in ./woocommerce-subscriptions/vendor/woocommerce/subscriptions-core/includes/wcs-resubscribe-functions.php, we see in its return that it provides another filter wcs_can_user_resubscribe_to_subscription — and ultimately — this is the one I prefer to use.

Using WordPress’s predefined function __return_false for the wcs_can_user_resubscribe_to_subscription filter denies anyone from resubscribing, effectively disabling the feature. Or you could implement your own logic in place of __return_false – another conversation for another time.

Hit me up with any questions, improvements, and/or thoughts you have =]

Cheers,

Ryan

Adding WordPress Post Metadata when using wp_insert_post

I found a little trick that I’ve overlooked so many times when writing plugins that use custom post types. Did you know that you can do this?

$args = [
    'post_type' => 'books',
    'post_title' => 'An Exciting Look at Mud',
    'post_content' => 'Things and stuff and content. Probably involving that book.',
    'meta_input' => [
        'book_author' => 'Mr. Bookwriter Dude',
        'authors_fav_quote' => "I have a belly button and HERE IT IS!"
    ]
];
$post_id = wp_insert_post($args);

How cool is that?! Now don’t laugh at me, but I looked for my postmeta in my DB because I’m always skeptical and look at this beauty:

mysql> select * from wp_postmeta where post_id=742444;
+---------+---------+-------------------+---------------------------------------+
| meta_id | post_id | meta_key          | meta_value                            |
+---------+---------+-------------------+---------------------------------------+
| 2779343 |  742444 | book_author       | Mr. Bookwriter Dude                   |
| 2779344 |  742444 | authors_fav_quote | I have a belly button and HERE IT IS! |
+---------+---------+-------------------+---------------------------------------+
2 rows in set (0.00 sec)

Nice ๐Ÿ˜Ž

Cheers!
Ryan

Adding a Page to your WooCommerce My Account Dashboard

So you’re writing a plugin and you want to add your own pages for your customers to use in their dashboards with its own integrated nav link. There’s just one problem: documentation for *how* to do this is…. lacking.

I just spent two hours chasing this one down; I gotchu.

There are three components (2 filters and 1 action, specifically) you’ll need to accomplish this:

  1. Add your menu item to the WooCommerce Nav (filter: woocommerce_account_menu_items )
  2. Add the Query Var to WooCommerce (filter: woocommerce_get_query_vars)
  3. Create a callback to render your content (action: woocommerce_account_{$variable_name}_endpoint)
  4. [Bonus] Detecting if the request is to your new Woocommerce Page

Step 1 puts our nav menu item into the WooCommerce nav on the left (along with dashboard, payment methods, addresses, etc). Step 2 tells Woo to register the endpoint in WordPress so that it’s a known/valid URL for our site. Lastly, step 3 tells Woo what to do when that url endpoint is requested – render stuff in the content area!

Doing it this way lets WooCommerce do the heavy lifting and should work no matter what you’ve modified your ‘my-account’ permalink to. Ultimately, it’s a pretty easy task; figuring this OUT however… Let’s just say that either I don’t know how to google, or docs are seemingly absent on this, so I’m glad you found me here =]

Honestly, this makes one wonder if it’s part of the business model for buying plugins for woo – but I digress…

For this Example

In this example, I’m going to add a page to the my-account dashboard area that allows my customers to see their spend ranking in the store. Other options might be if you want to create a wishlist feature for your e-commerce store, a shipping status monitor tab, or whatever – your reasons are your own, I’m just here to help you get there ๐Ÿ˜‰

What is your account page endpoint going to be? I’m going to call mine “spend-rank” – you’ll see how it determines a lot of things after this (including the actual hook names).

Ok – without further adieu!

Step 1: Adding your Menu item to the WooCommerce Nav

This is done through a simple filter. If my nav item is called “spend-rank”, here’s how it’ll look for me:

/**
 * Add our "Spend Rank" nav menu item as the second to last item (just above logout).
 *
 * @param   array $nav_items    Dictionary of WooCommerce Nav Items for the my-account page.
 * @return  array               The nav items with our spend-rank menu item added.
 */
function spend_rank_set_up_nav_item($nav_items)
{
    // Splice logout off the back of the list and add our spend-rank in just before it.
    $tail_items = array_merge(
        // Notice slug part => Human Readable Menu Item
        ['spend-rank' => 'Spend Rank'],
        array_splice( $nav_items, -1, 1 ) // last item should be logout o.O
    );

    // Smash them back together and return the results
    return array_merge($nav_items, $tail_items);
}
add_filter( 'woocommerce_account_menu_items', 'spend_rank_set_up_nav_item' );

Fire up the my-account dashboard on your WooCommerce site and you should see your menu item there. If you don’t, double check that you’ve lined up your filter callback to the function name and hopefully that gets you squared away.

If you’re wondering about all the array_splice‘ing and array_merge‘ing going on, it’s to keep the keys in place. Dictionaries in PHP are ordered, so it matters!

Step 2: Add the Query Var to WooCommerce

Not to just WordPressโ€ฆ to WooCommerce. This is specific to WooCommerce, so we’re hotwiring it using default Woo filters. Here’s where we tell WooCommerce (and therefore WordPress) about our url path /my-account/spend-rank/ – check this out:

/**
 * Adds our spend-rank query var directly into WooCommerce so it can do all the heavy lifting for us.
 *
 * @param   array   $query_vars Dictionary of WooCommerce query vars.
 * @return  array               Same dictionary with our spend-rank endpoint added in.
 */
function spend_rank_add_query_var($query_vars)
{
    // The key should correlate to your endpoint and be unique(!!!), but ultimately...
    // it's unused AFAIK.
    $query_vars['spend-rank'] = 'spend-rank';
    return $query_vars;
}
add_filter( 'woocommerce_get_query_vars', 'spend_rank_add_query_var' );

In order for this to take effect, we’ll need to re-save our permalinks, but let’s hold off on that for now since WooCommerce won’t know what to do for this yet. Now let’s wire up the render callback.

Step 3: Create a callback to render your content

WooCommerce has done all the heavy lifting for us, and now that we’re done describing that “spend-rank” is a thing to WooCommerce, we just need to tell it what to do in that context. So let’s give it a callback to render our content!

function spend_rank_render_content()
{
    echo "<h1>GO SPEND RANK, GOOOO!</h1>"; // whatever that means to you ;)
}
// Notice how we've added a magical action with "spend-rank" in the action name:
add_action( 'woocommerce_account_spend-rank_endpoint', 'spend_rank_render_content' );

Still doesn’t work? Oh yeah – don’t forget to re-save your permalinks! From the /wp-admin/ area: Settings -> Permalinks -> Save (no edits) OR perhaps on plugin/feature activation, you could try a flush_rewrite_rules() so your customers don’t have to do that manually.

Bonus: Contextually detecting your new WooCommerce Page

Here’s a quick bonus: what if you need to enqueue a special script or style but you’d rather it only be for your new page. You can use the is_wc_endpoint_url function that comes out of the box with WooCommerce. Check it out:

// Can only be run during hook `parse_request` @ $prio > 10 or after
if ( is_wc_endpoint_url( 'spend-rank' ) ) {
    // enqueue a script? wp_die()? You decide! =D
}

That’s pretty much it, y’all! Integrating your plugin’s functionality into WooCommerce should be the easy part. Hopefully this gets you a little closer. Now go write the hard part ๐Ÿ˜‰

Cheers,
Ryan

How to Easily Setup PHPUnit Tests for Codeigniter 3

This tutorial will show you how to take a new or existing Codeigniter 3 project, and add PHPunit test capabilities to it. While Codeigniter 3 shipped with its very own unit test suite, some of us still prefer PHPUnit. I’ve read many methods online, but wasn’t 100% able to do everything I needed for one reason or another. This method aims to fix that.

In essence, we’re going to create a phpunit config file, tell Codeigniter when to use our test configurations, create a custom test route, and write an inert test controller to get the app in scope for our tests. Once those pieces are in place – just add tests! Although this is a longer post, I promise it’s pretty easy. I’m just a verbose kinda guy – apologies if you’re in a hurry. Here’s the gist:

  1. Defining our Testing Environment
  2. Create a PHPUNIT Config File
  3. Configure Routes for Testing
  4. Creating Our Inert Test Controller
  5. Writing Your First Test
  6. Bonus: Writing an Actual Test for a Pretend Library

Let’s get started!

Step 1: Defining our Testing ENVIRONMENT

In the Codeigniter 3 index.php file, you’ll see a bunch of logic that tries to determine and define the ENVIRONMENT. Here’s where we’re going to modify some of this logic to tell the app when we’re running tests so it knows to load the testing set of configs.

To do this, we’ll look in the index.php file around line 53 for something like this:

define('ENVIRONMENT', isset($_SERVER['CI_ENV']) ? $_SERVER['CI_ENV'] : 'development');

And replace it entirely with this:

if ( defined("PHPUNIT_TEST") ) {
    define('ENVIRONMENT', 'testing');
} else {
    define('ENVIRONMENT', isset($_SERVER['CI_ENV']) ? $_SERVER['CI_ENV'] : 'development');
}

Effectively, when PHPUNIT_TEST is defined, we’ll set the ENVIRONMENT constant to “testing”. This will tell Codeigniter to overwrite any production configs with what’s in the “testing” folder. I’ll show you how to create that to modify certain CI configurations for our tests shortly.

(Optional step) I’ve left the default environment determination (set as development) in there, but if you want, it might be wise to assume the app should run as production, JIC it gets deployed somewhere the CI_ENV isn’t defined properly and you don’t want errors to spew all over the front page after a deploy has gone sideways. Just replace the second define with this:

define('ENVIRONMENT', isset($_SERVER['CI_ENV']) ? $_SERVER['CI_ENV'] : 'production');

Just a thought though ๐Ÿ˜‰

Step 2: Create a PHPUNIT Config File

Let’s assume that the Codeigniter 3 framework is installed in the root of your project. In this case, the index.php file, /application folder, /system folder, and maybe the /user_guide folder are all here. If this isn’t your setup, just know that the point here is to specify the Codigniter’s index.php file as the PHPUnit “bootstrap” file. For this tutorial, we’ll place our phpunit.xml file right here beside the index.php file and run our tests from that directory.

The contents of your phpunit.xml file should look something like this:

<?xml version="1.0" encoding="UTF-8" ?>
<phpunit bootstrap="index.php">
    <testsuites>
        <testsuite name="UnitTests">
            <directory>application/tests/unit</directory>
        </testsuite>
    </testsuites>
    <php>
        <ini name="display_errors" value="true"/>
        <const name="PHPUNIT_TEST" value="1" />
    </php>
</phpunit>

Here I’m making a few additional assumptions – all of which you can feel free to adjust to taste:

  1. We’re writing a unit test suite in this example
  2. We’ve placed our unit tests in the /application/tests/unit/ folder
  3. We want to display errors as they occur

If those 3 pieces are good with you, they’re good with me =]

You’ll also notice that there is a line that says <const name="PHPUNIT_TEST" value="1" /> in there – that’ll be defined when we use PHPUnit to invoke Codeigniter for our tests now!

Step 3: Configure Routes for Testing

Somehow we need to get the Codeigniter framework running and in scope for our tests. To do that, we’re going to create our “testing” environment’s routes.php config so we can tell Codeigniter where to route our testing requests (we’ll write the test controller in step 4).

Create a folder called “testing” inside the /application/config/ folder. Inside there, we’re going to overwrite the production routes.php by creating our own in /application/config/testing/routes.php. The inside of that file should look like this:

<?php

$route['default_controller'] = 'test/index';
$route['404_override'] = 'test/index'; // when in doubt, use the hammer
$route['translate_uri_dashes'] = FALSE;

At this point, feel free to create any other testing config files you need to overwrite any production values. Ensuring your tests aren’t running/accessing anything that could potentially *do things* should be paramount! Things you might want to consider are the /application/config/config.php values, the /application/config/database.php settings… etc.

Now we’re going to go create an empty Test controller to ensure our test request is inert.

Step 4: Creating Our Inert Test Controller

We want an empty controller for our tests. This does two things for us: 1) it gets the app within the scope of our tests so we can use the Codeigniter instance, and 2) it ensures that the controller doesn’t start doing actionable things to our data/system. In the case of our UnitTests suite, we only want to exercise the parts of the code we intend to.

Let’s create the test controller here: /application/controllers/Test.php and put the following code in it:

<?php

if ( !defined("PHPUNIT_TEST") ) {
    show_404();
}

class Test extends CI_Controller
{
    public function index()
    {
        // Yep... This is all we need. Like I said... Inert.
    }
}

Notice that we’ve placed a caveat at the top of this file. If PHPUNIT_TEST isn’t defined and /test is requested it’ll 404, or if this file is directly accessed, it’ll fatal with a “Undefined function: show_404()” message. Hopefully in production, that’s just a simple WSOD (white screen of death), but if someone’s pokin’ around here, a WSOD is totally acceptable… Of course, you are free to handle those situations as you see fit #GzipBomb =]

Also, I realize that maybe you have a MY_Controller class that you normally extend. Sometimes, it’s viable to skip extending that (and go straight to the CI_Controller) for your tests, and other times you might *need* to extend it. I’ve been in scenarios where I’ve had to extend the MY_Controller and overwrite methods and properties that do actionable things here in my Test controller (like user auth stuff, etc). Adjust to taste, but for this tutorial, we’ll stick with extending the CI_Controller.

Step 5: Writing Your First Test

Let’s create our first test by asserting that true is in fact true after we’ve pulled in the Codeigniter instance. Though trivial, this will help us ensure that our tests are running and we’ve got the framework at our fingertips. Create a test file here: /application/tests/unit/myFirstTest.php and we’ll put the following code inside:

<?php

use PHPUnit\Framework\TestCase;

class MyFirstTest extends TestCase
{
    private static $CI;

    public static function setUpBeforeClass(): void
    {
        self::$CI =& get_instance();
    }

    public function test_true(): void
    {
        $this->assertTrue(true);
    }
}

I’m using PHPUnit version 6.5.5 at the time of this writing, which requires that the methods specify their return data types. Also, this might not be backwards compatible if your PHPUnit version predates php 5.6 (IIRC), but you get the idea of what we’re trying to accomplish. Hit up your version of PHPUnit’s docs if you’re getting fatal errors extending the PHPUnit TestCase class like this.

Once this test file is in place, save it and lets run our tests from the CLI in the directory our phpunit.xml file is in:

>phpunit
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

.                                              1 / 1 (100%)

Time: 55 ms, Memory: 4.00MB

OK (1 test, 1 assertion)
>_

Heyyyyy! As it turns out, true is true – and running unit tests inside the context of CI is easy now too! You can use the CI instance to load your helpers/libraries/etc and exercise that code.

Sidenote: if you’ve been using PHPUnit for a while, you’re probably already familiar with the conventions, but JIC here they are:

  • file names of your tests end with “Test.php” (fooTest.php)
  • class names end with “Test” (class FooTest)
  • test methods START with “test” (public function testFoo(): void {})

If you’re getting a warning that no tests were found, make sure you have the conventions lined up and your test files are inside the folder you specified in the phpunit.xml file. In the example phpunit.xml file I gave, that folder is specified in the “UnitTests” TestSuite as /application/tests/unit and it *can* be inside a subfolder of that.

Bonus: Writing an Actual Test for a Pretend Library =]

Let’s look at what a real test for our app might look like. Let’s say we have a Hello library with a “hello world” method called who that we want to test. The library is appropriately placed in /application/libraries/Hello.php and our test file to exercise this code is in /application/tests/unit/libraries/helloTest.php. The code for our test could go something like this:

<?php

use PHPUnit\Framework\TestCase;

class HelloTest extends TestCase
{
    private static $CI;

    public static function setUpBeforeClass(): void
    {
        self::$CI =& get_instance();
        self::$CI->load->library('hello');
    }

    public function test_hello_world(): void
    {
        $expected = 'Hello World';
        $argument = 'World';
        // Here we test the Hello library's who method...
        $actual = self::$CI->hello->who($argument);
        $this->assertEquals($expected, $actual);
    }
}

This of course could exercise helper classes/functions that aren’t placed inside the CI instance class, or the tests could use a dataProvider – however you normally write your PHPUnit tests. Suffice to say, you still have access to using the framework almost exactly how you normally would inside your controllers.

Wrapping this up

So what all have we accomplished? We’ve created a way to flag Codeigniter to recognize we’re running tests, created a custom route config for our tests, built an inert controller to give PHPUnit access to our app, and wrote some tests!

I hope this tutorial was clear, helpful, and worked for you! Feel free to leave me any comments, questions, and/or feedback below.

Cheers!
Ryan

PS – sorry for waiting so long to write this. I hope it’s not too late.

How I Write Professional Code

Subtitle: and talk about it UNprofessionally

I’ve been writing code for about the last decade and a half. It really wasn’t till I started working on a team of developers that I was pushed to write better, more readable code — a very important lesson. Then, it wasn’t till I started breaking old habits and started writing in a test driven development (or TDD) fashion that I started to write professional code. Continue reading “How I Write Professional Code”

Unmasking the User in PHP CLI Scripts

Who are you. Whoo whoo. Whoo whoo.

…The Who? Anyone? …Bueller?

So let’s say you need to know what user is running a script in PHP. If you’re like me, you may have tried get_current_user only to find that’s just for the current owner of the file. Well THAT’S deceivingly unhelpful! =\

Wait! Sec. One more moment of gripe: If what get_current_user in php does makes sense to you, I’d like to be the first to ask you: …How?!

Not only did that not help, but I also couldn’t find any solutions or hints in the right direction from the comments in the documentation (generally, I find them to be a good place for a next step). There was one suggesting to using posix functions like this: print_r(posix_getpwuid(posix_geteuid())); — however, no luck! If the user is invoking the script with sudo, we lose all trace of who done it. Also, this is technique can limit who can use it as it’s not uncommon for environments to disable posix_* functions for security.

So, Who Really Done It?!

Ranting aside… the answer to this eluded me! Low and behold, the it was inside of $_SERVER all along. Le sigh. I put together a quick function with a few fallbacks to figure it out. Let me know if this doesn’t work for you, but for me, does the trick in spades =]

function get_real_current_user()
{
    if (isset($_SERVER['SUDO_USER'])) {
        return $_SERVER['SUDO_USER'] . ' [priv]';
    }
    if (isset($_SERVER['LOGNAME'])) {
        return $_SERVER['LOGNAME'];
    }
    if (isset($_SERVER['HTTP_HOST'])) {
        // Could also use $_SERVER['REQUEST_URI'] rather than SCRIPT_NAME... up to you =]
        return "Web via: " . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'];
    }
    return "Unknown User";
}

I like this solution the best. Not only is this data already in the scope of our script already, but we don’t have to use posix or try crap like echo exec("who am i"); and parsing through that. #winning

Hope that helps someone out there in the ether!

Cheers!
Ryan

One or many

So, I get that there are probably better ways to handle this circumstance, but when working with legacy code, sometimes, it’s not up to you. I had a method that would be called with either one thing, or an array of these things. Approaching a context like this, the solution we came up with was (what I think is an) amazingly simple way to ensure if you’re sometimes receiving one thing or an array of things, you can handle both.

Check it out:


class Foo
{
    public function oneOrMany($input)
    {
        if ( is_array( $input ) ) {
            return array_map(array($this,'oneOrMany'), $input);
        }

        // Now do stuff to one $input only and return your results =]
        return $results;
    }
}

There it is. And now hopefully I’ll remember this better next time I need something quick.

Cheers!
Ryan

Quick way to remove all tables in MySQL DB through *nix CLI

Tablenator!

Here’s a quick time saver (if you dev like me, it’ll come in handy!) for when you just want to drop all of the tables in a database. Of course, test it first so you know what you’ll be dropping!

Test First:

for I in mysql database_name -u user_name -p -e 'show tables' | awk '{ print $1}' | grep -v '^Tables' ; do echo "Table: $I" ; done

Code:

for I in `mysql database_name -u user_name -p -e 'show tables' | awk '{ print $1}' | grep -v '^Tables'` ; do mysql -u user_name -p -e "drop table $I" ; done

Rundown

The basic formula is a BASH for loop that grabs all of the tables in the desired database, filters the output using a combination of AWK and GREP so we can use just a simple list, then drops each table separately.

USE THIS CODE AT YOUR OWN RISK! BACKUP YOUR DATABASE FIRST AND HAVE A RECOVERY PLAN IN PLACE! (trust a man known to fat finger things)

SQL Adding a Column to an Existing Table

Since I can never remember the syntax to add a column to a table, here it is:

alter table foo_tablename add column foo_column int(10) default null

  • alter table – the actual command
  • foo_tablename – the table name we’re altering
  • add column – telling sql to add a column instead of some other alteration
  • foo_column – name of the column to add
  • int(10) default null – the column’s qualities (it’s an int up to 10 digits long, can be null and defaults to null

Tada!!!

Performance Issue Gut Check for MySQL

Sometimes MySQL performance can be difficult to hammer out. One of the quickest ways to get a good gut check is to have a look at the current process list. Check this out:

mysql> show processlist;
+-------+------+-----------+------------+---------+------+----------+------------------+
| Id    | User | Host      | db         | Command | Time | State    | Info             |
+-------+------+-----------+------------+---------+------+----------+------------------+
| 14302 | abcd | localhost | rohjay_one | Sleep   | 3696 |          | NULL             |
| 14958 | abcd | localhost | NULL       | Query   |    0 | starting | show processlist |
+-------+------+-----------+------------+---------+------+----------+------------------+
2 rows in set (0.00 sec)

mysql>

Now, this is relatively uninteresting, but sometimes, you’ll find a list of active queries that have stacked up against your db. Some things that can really help you identify issues is by ensuring each different piece of your platform identifies itself uniquely.

Once that happens, you can see which queries from where are hanging on. Parts of your platform can stall and hold onto connections, queries can take an eternity, bad code can hold result sets in memory, etc… This should give you a good idea where to start looking!

Hope this helps =]

Rohjaynator::1728412939::19622545