Going Beyond Hooks With WooCommerce

Development By James Ward, 17th December 2017

While WordPress hooks do offer a great deal of flexibility in customising core functionality, there are limitations to using them:

  • You are at the mercy of documentation and naming convention: if either of these are not taken into careful consideration it can lead to confusion and irritation as you try to figure out why something isn’t working the way you expect it to. We’ve all had those times where a hook doesn’t fire when we’re expecting it to – or maybe not at all. Most of the time it’s not down to our code but rather the miscommunication of the name of the hook and its accompanying documentation.
  • You do not have full control: If a hook does not exist for your needs then you’d need to find some workaround.

The second point is what we’ll be focusing on. Normally if a hook does not exist you’d have to find an alternative approach to the problem which often leads to more brittle, hacky solutions. However with WooCommerce this may not needed.

WooCommerce’s architecture leverages a more object oriented approach compared to the WordPress core, because of this we can alter functionality that doesn’t have any hooks without editing the core directly.

You may think that the scenarios in which a needed hook doesn’t exist are far and few between but this is exactly what happened to me: A third party was pushing updates to WooCommerce products via the REST API – everything worked great, until I noticed that product images would always be created even if the image already existed. This quickly grew to a huge problem with a very cluttered media library and lots of wasted disk space.

It’s a fairly simple problem to fix on the surface: run a check before inserting the image and only upload it if it doesn’t already exist. However, as you guessed no hook currently exists within WooCommerce to accomplish this, the way around this would be to pass the image ID rather than its url, unfortunately in my situation this wasn’t an option.

The solution is deceptively simple: all we need to do is extend the appropriate class and overwrite the necessary methods, this way the core can remain untouched. As the saying goes “the simplest solutions are often the best ones” so with that said let’s get started:

The first thing we need to do is create our class with a few stub methods:

class MyCustomProductsController extends extends WC_REST_Products_Controller
{
public function register_routes()
{
}

protected function prepare_object_for_database($request, $creating = false)
{
return parent::prepare_object_for_database($request, $creating);
}
}

The only thing of note here is the return statement inside of the prepare_object_for_database method. We’re doing this so we can hand responsibility back to the parent method once we’ve run our custom code, this way the WooCommerce core team can alter the contents of their prepare_object_for_database method and ours will not break as a result.

The last bit of boilerplate to get out of the way is our register_routes method, because we want the API to use our class rather than the core we need to re-register the endpoint. To do this we can just copy and paste the route we want from the WC_REST_Products_Controller class, the only change we need is to add the boolean ‘true’ as the final argument – all this does is re-register the endpoint rather than trying to create it. Because this is being done from our custom class, all instances of $this will reference our class rather than WC_REST_Products_Controller and because we’re not overriding any other methods they’ll default to WC_REST_Products_Controller when WooCommerce calls them.

With this done we’re now ready to add our image check to the prepare_object_for_database method. The final class looks as below:

class MyCustomProductsController extends WC_REST_Products_Controller
{
public function register_routes()
{
register_rest_route( $this->namespace, ‘/’ . $this->rest_base . ‘/(?P[\d]+)’, array(
‘args’ => array(
‘id’ => array(
‘description’ => __( ‘Unique identifier for the resource.’, ‘woocommerce’ ),
‘type’ => ‘integer’,
),
),
array(
‘methods’ => WP_REST_Server::READABLE,
‘callback’ => array( $this, ‘get_item’ ),
‘permission_callback’ => array( $this, ‘get_item_permissions_check’ ),
‘args’ => array(
‘context’ => $this->get_context_param( array(
‘default’ => ‘view’,
) ),
),
),
array(
‘methods’ => WP_REST_Server::EDITABLE,
‘callback’ => array( $this, ‘update_item’ ),
‘permission_callback’ => array( $this, ‘update_item_permissions_check’ ),
‘args’ => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
‘methods’ => WP_REST_Server::DELETABLE,
‘callback’ => array( $this, ‘delete_item’ ),
‘permission_callback’ => array( $this, ‘delete_item_permissions_check’ ),
‘args’ => array(
‘force’ => array(
‘default’ => false,
‘description’ => __( ‘Whether to bypass trash and force deletion.’, ‘woocommerce’ ),
‘type’ => ‘boolean’,
),
),
),
‘schema’ => array( $this, ‘get_public_item_schema’ ),
), true );
}

protected function prepare_object_for_database($request, $creating = false)
{
/**
* Check if the image already exists,
* if it does remove it from the request so it is not duplicated
*/
$images = $request[‘images’];

foreach ($images as $key => $image) {
$post = get_page_by_title($image[‘name’], OBJECT, ‘attachment’);

if (is_a($post, ‘WP_Post’)) {
unset($images[$key]);
}
}

$request->set_param(‘images’, $images);

return parent::prepare_object_for_database($request, $creating);
}
}

Then all we need to do is instantiate the class inside of the rest_api_init action:

add_action(‘rest_api_init’, function() {
$myCustomProductsController = new MyCustomProductsController();
$myCustomProductsController->register_routes();
});

And we’re done! Now whenever a product is created via the API we check if it already exists, if it does we remove it from the parameter and send the request on its way.

In conclusion hooks are very powerful, but by their nature will never be the silver bullet to solve all problems, you’ll often be able to override the classes and methods you’d like to customise all while not touching the core.

If you want to learn more about some of Plug & Plays development fixes contact us today.