Overriding WooCommerce (or any WordPress plugin) functions: a semi-solution

Here’s a question I’ve spent the past few days dealing with based on an issue a client had. How can you override a function in the WooCommerce core? The simple and dismissive answer is, you can’t. I tried a lot of different possible solutions. Eventually you may just be able to track down a solution that works in your specific case, but there’s no general rule that works universally. In this post I am going to discuss the specific problem that was the impetus for this journey, explore some of the challenges the situation presents, and describe the ugly but marginally tolerable solution that worked in this one narrow case.

What’s the problem?

Why would you ever want to override existing WooCommerce functionality? Well, maybe I need to start with a simpler question. What is WooCommerce? It’s a plugin for WordPress that adds extensive e-commerce capabilities. It’s hugely popular and, for the most part, really good. But it’s not perfect, and it’s not right for every possible scenario.

We have a client with a huge number of products, and a huge number of variants of those products. Sizes, colors, styles, etc. And as it happens, this client doesn’t give all of those variants their own SKU. But they do give some of them a SKU, and sometimes multiple variants share the same SKU.

Don’t get all principled about the definition of a SKU. They’re totally arbitrary and different businesses will use them in different ways. It’s not my job to police my clients’ SKU conventions, just to give them a way to use them within WooCommerce.

So now, here’s the problem. The client had been able to give multiple variants the same SKU. But a couple months back, we ran an available WooCommerce update, and it “broke” this “feature” for them. As it happens it was really fixing this bug in the system that allowed variants with duplicate SKUs. At least that was the argument in favor of the change. Even though I agree with coenjacobs who wrote: “I can think of use cases where this might be expected behaviour. Variations don’t necessarily require a product to be completely different, therefore duplicate SKUs might be required.”

Yes, I can think of use cases like that… my client’s website! Unfortunately there’s no turning back, so we need to find a solution.

The solution, on the surface, is extremely simple. Inside the WooCommerce plugin there’s a deeply buried file called includes/admin/meta-boxes/class-wc-meta-box-product-data.php that has this bit of code at line 1547:

$unique_sku = wc_product_has_unique_sku( $variation_id, $new_sku );

That code happens inside a method that saves variant data. It’s calling a function that checks that the SKU on the variant is unique. This block of code was apparently added in version 2.1.12 of WooCommerce. All you really need to do to “fix” the problem and restore the old functionality that allowed duplicates is to comment out that line of code, and replace it with this:

$unique_sku = true;

(OK, technically it would probably be better to write another function that makes sure that the SKU is unique in the system except within variants of this particular product, but that’s a topic for another day… or at least a passing mention again at the end of this post.)

I put “fix” in quotation marks up above because, trust me, you’re not fixing anything by editing the core files of a plugin directly. You’re just setting yourself up for future problems. Next time there’s an update, and a well-meaning person runs it — say, oh, I don’t know… you — your changes will get wiped out. You need a way to make your changes somewhere else. You need to be able to override this functionality without changing the plugin itself. This is WordPress 101, people. Maybe you need a refresher course. (It’s all ball bearings nowadays!)

What makes this so difficult?

As it happens, there are a number of exacerbating factors that make fixing this particular problem in a safe way incredibly challenging.

1) PHP doesn’t let you override functions that have already been declared. If it did, I could very easily just redefine wc_product_has_unique_sku() in my own way, a way that does mostly what it currently does, but with the added allowance for duplicate SKUs for variants of the same product.

One potential solution I tried pursuing, even though I knew it wouldn’t work, was to “redefine” the function inside an if (function_exists()) conditional. This is a fairly common feature of WordPress and some plugins that is called making functions “pluggable”. The idea is that if you declare your function inside that conditional, and it’s already been declared somewhere else earlier, no problem. Files inside your theme are loaded first, so this allows you to write your own versions of pluggable functions and override the defaults.

But this “pluggable” conditional has to be in the core files in order to work. I can’t put that conditional into my functions.php file, because either it loads after the plugin (it doesn’t, but bear with me for sake of argument) and doesn’t do anything, or (as it actually happens) it loads before the plugin, declares the function, and then when the plugin loads PHP throws a fatal error.

Like I said, I knew this wouldn’t work, for the exact reasons described above. But I tried it anyway. Guess what. It didn’t work. Fatal error. Moving on…

2) WordPress offers “hooks” to allow you to override functionality, but only if that functionality uses a hook. Not every function call in WordPress, or in plugins, uses hooks. A lot of them do, and that’s great. But the call to wc_product_has_unique_sku() at line 1547 in the save_variations() method of the WC_Meta_Box_Product_Data class, found inside that aforementioned class-wc-meta-box-product-data.php file, does not. At least not directly. So the only way this solution will work (hint, hint) is if you can find a place farther up the logic chain that does.

3) The one way PHP does let you override functions is through a PECL extension that is probably not installed on your web server. If I were hosting this client’s site myself, on my own server, I’d just install the necessary PECL extension so the override_function() function was available to me. But I’m not, so I can’t.

4) As designed, WooCommerce only allows you to override files inside its templates subdirectory. You may see a lot out there about solving all of these kinds of problems I’m describing by simply creating a woocommerce directory inside your theme and replicating files from WooCommerce there. Yeah, you can do that, and it works great. As long as the file you’re trying to override is in the templates directory of the main WooCommerce plugin. Anything else is untouchable. Trust me. I tried.

But… you said you found a solution, right?

Yes. I did.

Well… what is it?

You’re not going to like it.

A solution is a solution. C’mon, just tell me!

OK, fine. I found a solution that is ugly and messy, and only works in my specific case. And it’s not at all the kind of ideal “replace this function with my own” solution I wanted.

I did a manual backtrace-of-sorts on the functionality surrounding line 1547 in class-wc-meta-box-product-data.php to see if I could find an action, somewhere, that I could hook into. And I did. But it isn’t pretty.

You see, like I said, line 1547, the one and only line in the entire body of WooCommerce that I want to change — I don’t even want to edit the wc_product_has_unique_sku() function because it’s necessary elsewhere; I just want to not run it right here — is deeply nestled inside a delightfully complex method called save_variations() that weighs in at 263 lines of code. What’s worse, this method is only called in one place in the entirety of WooCommerce, and that’s in the save() method within the same file. Guess what… the save() method is 434 lines of code. Yikes.

Next up, I needed to find any places in the WooCommerce code base where that method appears. And as it happens it only appears in one place… and it’s an add_action() call! At last, we have a hook!

In the file includes/admin/class-wc-admin-meta-boxes.php, at line 50, we have the following:

add_action( ‘woocommerce_process_product_meta’, ‘WC_Meta_Box_Product_Data::save’, 10, 2 );

That’s it. One static instance of the method, inside an action. That’s something we can use, but… oh, no. Does that really mean… but wait, you’re sure there’s… uh… oh.

Yes, we can override this method by creating our own class that extends WC_Meta_Box_Product_Data, and then removing this action and replacing it with another one that runs the save() method inside our class instead. And we can change anything we want inside that save() class. And it will work. I tried it.

But it means we have to replicate all of the rest of that code inside our custom class. We don’t have to replace any of the methods of the parent class that we’re not changing. But we have to replace both the save_variations() method and the save() method that calls it, because at line 1442, there’s… this:

self::save_variations( $post_id, $post );

That self means that if we just change save_variations() in our class, it’ll never run. We need to bring over the entirety of both of those methods into our class — all combined 697 lines of code — just to replace that one line.

That said… it does work. And it’s safe from future WooCommerce plugin updates. As long as nothing else in those two methods gets changed.

Practically speaking, it means remembering before every WooCommerce update to do a diff on class-wc-admin-meta-boxes.php between the new version and the version you’re currently running, then making sure any changes that appear in those two methods get copied over into your child class.

Is this really any better than editing the core and just remembering to redo your change every time there’s an update? I think you could argue that it’s not, but I’ll still say it is, because at least your code won’t disappear when the update gets run. Something still might break… but if this file hasn’t been changed, it won’t. Editing the core file directly guarantees your fix will break when an update runs, and it’s much harder to get your code back. (Hope you kept a backup.)

Let’s put it all together

Now that you’ve seen what we need to do, let’s do it. This goes into your functions.php file in your theme, or, as I have it, into a separate file that gets include()’d into your functions.php file.

<?php

class My_WC_Meta_Box_Product_Data extends WC_Meta_Box_Product_Data {

  public static function save( $post_id, $post ) {
    // Yes, put the ENTIRE WC_Meta_Box_Product_Data::save() method in here, unchanged.
  }

  public static function save_variations( $post_id, $post ) {
    // Again, the ENTIRE WC_Meta_Box_Product_Data::save_variations() method goes in here,
    // unchanged EXCEPT for this line, which you comment out and replace with the next:
    // $unique_sku = wc_product_has_unique_sku( $variation_id, $new_sku );
    $unique_sku = true;
    // Everything else that came after in the original method goes here too.
  }
}

remove_action('woocommerce_process_product_meta', 'WC_Meta_Box_Product_Data::save', 10);
add_action('woocommerce_process_product_meta', 'My_WC_Meta_Box_Product_Data::save', 10);

?>

Well, that’s profoundly unsatisfying

I agree. This is not the solution I was looking for. I’m sure it’s not the solution you’re looking for. But for now at least it seems like the best only way.

What I would really like as a solution requires changes to the WooCommerce core:

1) This would really be great: a checkbox option in the WooCommerce settings to allow duplicate SKUs for variants. I built my own CMS and used it for almost every project for years before fully embracing WordPress as a general purpose CMS. Eventually, it got to a point where some clients were asking for functionality changes that might conflict with ways other clients were using the system. Any time I introduced a significant functionality change, even if it was fixing a “bug” that some clients might assume was a “feature”, I always set it up as a configurable option in the system, with the default setting being the way it already was.

2) Not as great, but this would still help: all WooCommerce functions being made “pluggable”. If only the declaration of the wc_product_has_unique_sku() function inside wc-product-functions.php were wrapped inside an if (function_exists()) conditional, I would be able to write my own version of that function that allowed duplicate variant SKUs. In fact that would be a much more powerful solution than what I’ve done, because I could write it to make sure the variants’ SKUs were only duplicates within the same product. I haven’t tested it thoroughly yet, but I’m pretty sure my actual solution will do nothing to prevent duplicate SKUs for variants of different products, as well as the same product.

Here’s hoping someone with commit rights on the WooCommerce project sees these suggestions and acts on them. In the meantime, I’ll have to live with my convoluted solution. I hope this helps shed some light on ways you might be able to find your own way around this sticky problem.

Last, This, Next

As I was folding a week’s subset of my embarrassingly large collection of printed t-shirts, I reflected momentarily on the history of my pixelated Minnesota t-shirt. I bought that t-shirt last summer and wore it each time I went to the Minnesota State Fair last year, as my symbol of “Minnesota pride”.

Then I started thinking about sharing this story, and about referring to the Minnesota State Fair that took place in 2013 as the “last” Minnesota State Fair, and how the one that will take place “this” year, in 2014, is “this” State Fair, and so on.

Frequently conversations between SLP and me have resulted in confusion based on the different possible interpretations of “last”, “this” and “next” when referring to days, weeks, months, years or events. I tend to use “this” when I’m referring to any unit of time that occurs within the same larger unit of time, whether before or after the current one, although I may be likely to omit “this”.

For example, today is Thursday. The Super Bowl (or, if you prefer, the Suberb Owl) is happening in 3 days. It’s happening “this Sunday”. But what if today was (or is it “were”? I never get that right, either) already “Superb Owl Sunday” and I was (“were”?) talking about the 5K race I’m running in 7 days later? “This” Saturday seems a bit far off in that case. But “next” Saturday doesn’t feel right to me either. Or does it? Is it better for “next” Saturday to refer to a day that’s 6 days away, or 13?

As for my confusion with SLP, the fact that she lived her life on the September-to-June academic calendar for much longer than I did only exacerbated the situation. I’ve always been a stickler (to the point of ridiculousness) for precision in dates. The first day out of school isn’t the beginning of summer; the solstice is. The first day back in school isn’t the beginning of fall; the equinox is. And the first day back in school in late August or early September most definitely is not the beginning of the new year. (Although yes, Rosh Hashanah usually does occur in September so depending on the calendar you use, there’s an argument to be made.)

Ironically, it was only after SLP stopped organizing her life around the academic year that I embraced calling any of the days in early-to-mid June when our kids are out of school (but which are still technically in spring) “summer”, but I will never give up the idea that “this year” refers to the 4-digit number starting with a “2” that comes at the end of the current date. “This year,” to me, means January 1 to December 31. Period.

But what do I mean when I say “this winter”? Sure, winter technically only starts about 10 days before the new year, so it’s almost entirely in 2014. But let’s be honest. In Minnesota, “winter” usually starts in early December, or sometimes as early as October. By my logic, “winter” in Minnesota begins on whatever day snow falls and doesn’t melt away. We had a few light snows in November, but “this winter” began on December 2, 2013.

My point is: language is fuzzy. Assigning vague labels like “last”, “this” and “next” to our days and events relies on a great deal of tacit agreement between ourselves over meaning. This particular quirk of our language has been causing me trouble since I was a kid. Back then I had a lot of time, sitting around bored in school (which I didn’t even realize was the case until much later in life), to ponder and obsess over and get annoyed by things like this. I was trying to create in my mind a world of precision and clarity that didn’t, and couldn’t, exist. Our minds don’t work that way, the world doesn’t work that way, and language, a product of our minds used to help us understand and communicate with each other about the world, necessarily can’t work that way.

I didn’t understand that then, and I only barely do now. Each of us carries around an entire universe in our mind. It’s built on a foundation laid by our genes and constructed around our experiences — and our interpretations of those experiences. Our language can only achieve an approximation of a fraction of that universe, and we have to rely on the assumption that our own version of the language we use is a close enough approximation of the same things in our own mental universe as the language, and the mental universe it represents, of the others around us.

It’s a wonder we can communicate at all.

SPF for dummies (i.e. me)

For a while I’ve known that (legitimate) outgoing email messages originating from my web server were occasionally not reaching their intended recipients. I also knew that there was a DNS change you could make to help prevent this problem, but I didn’t know any more about it and it was a marginal enough problem that I could just put it off.

Finally today I decided to deal with it. And I was (re)introduced to the SPF acronym. No, that’s not Sun Protection Factor, or Spray Polyurethane Foam, or even Single Point of Failure (although in my case perhaps that last one is accurate). No, it stands for Sender Policy Framework, and it’s an add-on to the core capabilities of DNS that provides a way to positively identify the originating servers of outgoing email messages.

My situation is simple: I have a domain name that needs to be able to send mail from either my mail server or my web server. Most of the tutorials I found for SPF were far too convoluted to address this simple arrangement. Then I found this post by Cyril Mazur which provided the very simple answer:

v=spf1 a mx ~all

Simply add the above as a new TXT record in your DNS zone file, and you should be set.

Parental guidance is suggested

I don’t write a lot here about the fact that I’m a parent. I certainly don’t try to hide it. I regularly tweet my 5-year-old daughter’s witticisms, and I post pictures of her and her 8-year-old brother on Instagram all the time. But I don’t blog about it because, well, I don’t really feel like I have that much of value to say on the matter.

I’m not a great parent. I’m not a bad parent, but I’m not one of those super-engaged, every-day-is-inspired-genius, my-children-are-the-center-of-my-universe Parents. I’m just a dude who’s married and has a couple of kids. We make sure they’re fed, bathed regularly, do their homework, brush their teeth, all of that stuff. On the weekends we try to take them out and do things that are fun, intellectually stimulating, or (ideally) both.

So, I get a passing grade in the parenting department. But whatever you do, don’t come looking to me for parenting advice.

We’re not exactly (tie-)dyed-in-the-wool hippies, but like Steven and Elyse Keaton, one of our biggest fears is probably that our son will grow up to want to wear suits to school and believe in trickle-down economics. Politically, we’re pretty far to the left (at least by U.S. standards). We value and respect a diversity of perspectives, and if we teach our kids anything in life we want it to be to respect other people, and ways of being that may be different from their own. We also want them to be independent thinkers and to question authority.

The problem then arises that we may be too reluctant to teach them our own perspectives and values and beliefs. I sometimes wonder where the line is between filling kids’ heads with (the wrong) ideas, and not filling their heads with anything at all. Where does a careful effort not to impose ways of thinking and being cross over into not encouraging them to think, period?

Our kids are smart. They’re excelling in school. Yet sometimes they seem to lack “common sense.” That idea of common sense can be a tricky one, and is something we are especially trying to avoid. Because while just about anyone can say it’s “common sense” not to put your hand on a hot stove, where does common sense stop being “common”; when does it stop making “sense”? There was a time when slavery was “common sense.” It’s still “common sense” to some people that women should make less money than men for doing the same work. We’re currently in the middle of a national struggle to overcome the idea that it’s “common sense” that gay couples shouldn’t enjoy the same rights as heterosexual couples. Common sense, in other words, is often shorthand for assumed prejudices, because it’s hard to argue with “common sense.”

Just yesterday, as our family walked home from the LRT station, we were discussing the fact that even though our kids are so “smart,” we still don’t trust them to do “common sense” things like cross the street by themselves. I mentioned how, from first grade on, I walked six blocks to school by myself (well, with a neighbor who also went to my school and was two years older). My parents knew that I could find my way safely to and from a location a half mile away, five days a week.

A couple of days ago, I overheard our kids in the living room, discussing whether or not they believed in ghosts, and I was dismayed when they agreed that, yes, they both believed in ghosts. What?! As a science-minded agnostic (leaning atheist, but absence of evidence does not constitute proof against), I was upset to hear this. But as a let-them-decide-for-themselves liberal parent, I said nothing. I was hoping that the fact that they even felt the need to question whether or not ghosts were real was a good enough start for now.

I grew up Lutheran. Went to church most Sundays, went to Sunday school through high school. Beyond religion, my parents imparted most of their beliefs about the world and how to live in it directly to me, without all of this namby-pamby moral relativism I’m using to hold back my subjective opinions on certain topics with my own kids. (Fortunately, they were — and are — die-hard Democrats.) I avoided burning my hands on the stove, or running out into the street in front of a moving car, not through my own independent discovery, but because my parents told me not to.

I do think we live in a time when parents are expected to allow their children to discover for themselves, and to treat them as precious snowflakes, rather than to teach them stern lessons about the cruel realities of the world. (And we’re seeing the results of that approach as a generation grows up and never leaves home.) At the same time, I wonder if perhaps we, specifically, are taking certain aspects of that philosophy too far, even as we intend to counteract it. Children do need guidance to learn how the world works. And trying too hard to avoid accidentally imparting your own unconscious prejudices on them might sometimes lead to not even teaching them those things that truly are “common sense,” but still need to be taught.