For the last year, I’ve been periodically working on a massive WordPress project: an international multi-vendor marketplace integrated with a file sync and sharing application. It does pretty much everything. Since the client is a startup and we’re essentially prototyping the whole solution, the client decided to use as many 3rd party plugins as possible to save on the initial development time and cost. Thanks to this project, I’m now intimately familiar with the codebases of lots of WooCommerce plugins.
Here are some of the most important things I’ve learned from working on that project, in no particular order. If you have experience working with legacy code, some of this is general advice that’s quite obvious for you. However, there are also some helpful Wordpress-specific tips.
Before using a class or function, check if it exists
You have a bug somewhere and you want to know which of the 3 recently installed plugins caused it. The easiest way is to disable each plugin and check if the bug is still present, right?
However, if you’ve already written code that interfaces with one of those plugins, you’ll just get a fatal error after turning the plugin off.
Solution: always check if the function or class exists by using class_exists() and function_exists().
A simple, readable pattern is to return from a function if a required component doesn’t exist:
<?php add_action('init', function() { if (!class_exists('WC_Subscriptions_Switcher')) { return; } WC_Subscriptions_Switcher::remove_print_switch_link(); });
Accessing a plugin class instance
Let’s assume you want to remove an action to get rid of some function – but that function is defined inside a class.
Some typical situations:
Global variable
<?php class FooPlugin { public function __construct() { add_action('init', [$this, 'foo']); } public function foo() { // do something useless } } $fooPlugin = new FooPlugin();
This one is simple – the class instance is stored in a global variable. Access it like this:
<?php global $fooPlugin; remove_action('init', [$fooPlugin, 'foo']);
Singleton
If you’re lucky, the class you’re looking for is a singleton. It might look like this:
<?php class FooSingleton { private static $instance; protected function __construct() { add_action('init', [$this, 'foo']); } public function foo() { // do something useless } public static function get_instance() { if (null === static::$instance) { static::$instance = new static(); } return static::$instance; } }
In this case, it’s also super easy and safe to access it:
<?php $fooSingleton = FooSingleton::get_instance(); remove_action('init', [$fooSingleton , 'foo']);
Everything else
There’s also the chance that the plugin class is initialized without assigining it to a variable, which means you’re screwed.
<?php class Foo { // snip } new Foo();
If you just need to get rid of an action, there’s a tool for that. However, if you need to access something else inside that class..to my knowledge, it’s not possible. Write to the plugin support and ask for help!
By the way, if you’re wondering what’s the best and cleanest way to instantiate a class in your own plugin, I have another post for you.
Missing action parameters
Even if a plugin uses lots of actions and filters in its code, you might still discover that just that one really important filter is missing. Or maybe that filter exists, but you’d really need to access another variable which is not injected into the function. Here are some ideas for solving these situations. A word of warning – I would personally qualify them as “dirty hacks.” In terms of code readability they suck, so please don’t forget to document both what exactly they’re doing and for the love of God, why.
Let’s assume you have a membership site with multiple membership levels. And when a user who already has a membership level purchases another membership level, you want to assign them a third, separate membership level. Let’s also assume that you’re using Paid Memberships Pro (because that’s what I was using when I ran into this issue).
The relevant function from PMP looks something like this (it’s actually almost 200 lines of code, but I only left in the important stuff):
<?php function pmpro_changeMembershipLevel($level, $user_id = NULL, $old_level_status = 'inactive') { /** * SNIP */ /** * Action to run before the membership level changes. * * @param int $level_id ID of the level changed to. * @param int $user_id ID of the user changed. * @param array $old_levels array of prior levels the user belonged to. */ do_action("pmpro_before_change_membership_level", $level_id, $user_id, $old_levels); /** * SNIP */ //cancel any other subscriptions they have (updates pmpro_membership_orders table) if($pmpro_cancel_previous_subscriptions) { $other_order_ids = $wpdb->get_col("SELECT id FROM $wpdb->pmpro_membership_orders WHERE user_id = '" . $user_id . "' AND status = 'success' ORDER BY id DESC"); foreach($other_order_ids as $order_id) { $c_order = new MemberOrder($order_id); $c_order->cancel(); if(!empty($c_order->error)) $pmpro_error = $c_order->error; } } //insert current membership if(!empty($level)) //are we getting a new one or just cancelling the old ones { if(is_array($level)) { /** * SNIP */ } } /** * Action to run after the membership level changes. * * @param int $level_id ID of the level changed to. * @param int $user_id ID of the user changed. */ do_action("pmpro_after_change_membership_level", $level_id, $user_id); return true; }
So what we want to do is:
- Check the user’s old membership level,
- Check the membership level the user just purchased,
- Depending on #1 and #2, maybe assign them a completely new membership level.
We could try to use ‘pmpro_before_change_membership_level’ action to check both $level_id (currently purchased level) and $old_levels and then assign a new level to the user. However, as you can see, after that action is triggered, PMP cancels all currently existing subscriptions and assigns the new one – so this wouldn’t really work.
We could try to use the ‘pmpro_after_change_membership_level’ action, but as you can see, there’s no way to access the $old_levels variable from there.
Or maybe there is a way?
<?php // Bind our function to the FIRST action add_action("pmpro_before_change_membership_level", "codelight_pmp_override_level", 10, 3); function codelight_pmp_override_level($level_id, $user_id, $old_levels) { // If the user doesn't already have a membership level, do nothing. if (empty($old_levels)) { return; } // Remove self remove_action("pmpro_before_change_membership_level", "bs_pmp_override_level", 10, 3); // Bind a second function to the SECOND action // and pass $old_levels into that function. add_action('pmpro_after_change_membership_level', function($level_id, $user_id) use ($old_levels) { if ($old_levels) { pmpro_changeMembershipLevel(SOME_NEW_LEVEL, $user_id); } }, PHP_INT_MAX, 2); }
What happens here is that we register a function to the first action, where we have access to $old_levels. That function registers a second function to the second action and passes $old_levels with the ‘use’ keyword. The first action is removed to avoid an infinite loop, since we’re calling the same function pmpro_changeMembershipLevel() again to register the new membership level.
This is the ultra-clever, yet rather unreadable way to solve the problem. A simple alternative would be using the ‘global’ keyword.
<?php function codelight_pmp_globalize_old_levels($level_id, $user_id, $old_levels) { global $stupid_pmp_old_levels; $stupid_pmp_old_levels = $old_levels; } add_action("pmpro_before_change_membership_level", "codelight_pmp_globalize_old_levels", 10, 3); function codelight_override_level($level_id, $user_id) { global $stupid_pmp_old_levels; // We still don't want that infinite loop remove_action('pmpro_after_change_membership_level', 'codelight_override_level', 10, 3); if ($stupid_pmp_old_levels) { pmpro_changeMembershipLevel(SOME_NEW_LEVEL, $user_id); } } add_action('pmpro_after_change_membership_level', 'codelight_override_level', 10, 3);
This has the advantage of being simple and the pseudo-disadvantage of using a global variable.
Missing actions or filters
A more typical scenario is one where there are no actions at all in the plugin code. Some clever, yet really bad and unmaintainable ways to work around that issue involve using WP’s native actions and filters. There are lots of them, so whenever the plugin calls a WordPress core function, go dig inside the code base to see if there are any actions or filters there. A simple example: update_post_meta().
A more abstract example this time. Plugin code:
<?php function stupid_plugin_does_things() { // Stuff happens here update_post_meta('stupid_plugin_field', 'yes'); }
Now, assuming you want to run a function after stupid_plugin_does_things() function is called and has done all of its things:
<?php add_action( 'updated_postmeta', function($meta_id, $object_id, $meta_key, $meta_value) { if ('stupid_plugin_field' !== $meta_key) { return; } // DO YOUR THING HERE }, 10, 4);
There are lots of WP core hooks you can use like this, but for your own long-term sanity I also recommend writing to the plugin developer and politely asking them to add an action wherever you need.
Use PHPStorm
https://confluence.jetbrains.com/display/PhpStorm/WordPress+Development+using+PhpStorm
There are two really painful issues with 3rd party plugins.
Finding actions & filters
PHPStorm allows you to jump to the file where an action is triggered. Need I say more?
Delicious Brains has a really good article with some more examples.
https://deliciousbrains.com/how-we-use-phpstorm-wordpress-development/
Finding functions
In my experience, almost no plugin ever uses a standardized file name or folder structure convention – not to even mention namespaces. And most of them are structured in a way that makes sense only for the author(s). Here’s a random example from a plugin:
/wp-content/plugins/[plugin]/classes/
/wp-content/plugins/[plugin]/includes/
Some files use a dot as a separator, some files use a dash. Some files have the “adq” prefix, some don’t.
class.adq.shipping.php contains a class called ADQ_Shipping while class.static.quote.php contains a class called StaticAdqQuoteRequest.
And no, you are not going to figure out if the function you are looking for is located in adq-frontend-quote-request.php or adq-order-quote.php.
Anyway, my point is – do yourself a huge favor, save tons of time and use an IDE that supports jumping to function declarations and WP actions.
Get in touch with support
If there’s a bug or just something you cannot customize properly in a plugin that could be easily solved by tweaking a couple of lines, please talk to support or the plugin author. You’re not making just your own life easier, but this will save a lot of time for some other poor developer somewhere. In my experience, plugin authors are generally happy to add various actions or filters somewhere and sometimes even make bigger modifications to their code.
Document
Did you just spend 3 hours going through a plugin’s code to figure out how exactly it solves a specific problem and why it does so in a very strange, illogical way? Document it. Otherwise, in 6 months, you’ll need to do the exact same thing again.
Manage your client’s expectations ..
Yes, that widely used premium plugin you just purchased from WooThemes for $199 might have a bug. Yes, it might be a rather obvious, critical bug. Many developers don’t really test their code much before releasing plugin updates and instead rely on clients’ bug reports. This is an unknown factor that’s out of your control. But it means you’ll end up doing extra work by:
- Debugging the problem,
- Explaining to the client that a 3rd party component is faulty,
- Explaining to the 3rd party component’s customer support that their component is faulty,
- Convincing the 3rd party component’s customer support that it actually is their fault,
- Updating and testing the updated version of that component (and possibly going back to step #3).
And this might postpone the project deadline by several days or weeks, depending on the severity of the issue.
.. and test everything!
To continue the last thought – if you update a production site without testing properly in a staging environment before, a critical bug might end up in your online store and you’ll end up losing a few days’ worth of sales because customers can’t order anything. Be aware of that and allocate time and budget for testing new releases.
Know the risks
The plugin you just integrated to your super-complex custom solution might change at any time. The function you used somewhere might not exist in the next update. The class you instantiated somewhere might be completely refactored in the next update. With smaller plugins, you never know. Be aware that by extending a plugin that’s not really meant to be extended, you’re probably increasing the future maintenance costs of your code. Make an informed choice.