DEV Community

Lee Noble
Lee Noble

Posted on • Updated on

Preventing free-range Wordpress hooks

One of the major frustrations I experience when developing for WordPress is the abundance of functionality that relies on writing opaque strings. The most common of these are Actions and Filters. Pretty much every tutorial or piece of documentation you might find on creating and applying filters will present you with code like this:

add_filter('post_class', 'my_post_class_modifier', 10);

function my_post_class_modifier($classes){
    if($myConditionIsMet){
      $classes[] = 'another-css-class';
    }
    return $classes;
}

I've always had a disdain for this approach. It requires:

  • Prefixing the function name with a clunky namespace to avoid potential clashes
  • Writing the function name twice, one of which is inside a string, so Intellisense won't be any help

You won't be able to option-click on the function name and find out where it's being called, or find the function by clicking on the calling code. This is more of an issue when you need to use the same function for multiple filters. Your function gets further and further away from the calling code and unless you're very organised your functions.php file (which by default is where you'll put this code) will end up becoming an unnavigable dumping area full of nasty opaque string function calls. Ack!

Object method hooks

Ok, so most of the time I am a little bit cleaner in my approach and I'll try to corral hooks related to eachother or to particular custom post types into a class. You can of course use object methods in your hooks.

class myClass {
    public function applyHooks(){
        add_action('hook_string', array($this, 'hookForThisThing'), 10);
    }

    public function hookForThisThing($arg){
        // Perform an action here
    }
}

This approach means you don't have to prefix faux namespaces to function names and it does at least allow you to more easily find the original priority value - you could even make it a property of the class.

You can also use class properties inside your function's body which is a nice way of passing arbitrary data to the conditionals within. But you still have to use opaque strings which I'm frankly just against. I want my IDE to write as much of my code as possible as there's less room for typographic errors. Remembering function names is also an issue. A proper IDE will help you find it even if you only know one word in its name (a good reason for writing clear and long if necessary function names).

Anonymous functions

One approach I've started to use more frequently is to just pass an anonymous function to the filter:

add_filter('post_class', function($classes){
    if($myConditionIsMet){
      $classes[] = 'another-css-class';
    }
    return $classes;
}, 10);

This has a few advantages:

  • No clunkily named loose function knocking around the functions file
  • The hook being used is adjacent to the function.
  • You can pass in local variables if you need to like this:
$mySpecialValue = 87;
add_filter('post_class', function($classes) use ($mySpecialValue) {
    if(count($classes) == $mySpecialValue){
      $classes[] = 'has-88-css-classes';
    }
    return $classes;
}, 10);

But disadvantages include the inability to use the function on multiple hooks, and crucially the ability to REMOVE the hooked function if you need to later. You can't use the WordPress remove_filter method using anonymous functions.

For the vast majority of cases though, this has worked well for me. I could always define the anonymous function as a variable if I needed to use it on multiple filters but that removal thing became a problem that I needed to solve.

The problem

I had a bit of a hacky filter that I needed to run on 'posts_where' to modify the main query. Once applied to the main query, however, it was subsequently applied to any and all other queries used within the page building process. In the past I'd removed this type of filter by use of the overly-convoluted method of applying an action to remove the filter on some arbitrary filter following the main query being run. This results in more cruft drifting about in the functions file. It also requires recall of the priority value since the removal parameters must match. If at some point in the future you, or another developer (you of the future is another developer), changes the priority on the original hook then your remove operation (which is buried in the body of an unrelated function somewhere else) won't work any more. Your IDE won't tell you about it either since this code is impenetrable.

I decided to see if I could make self-removing filter.

Introducing the Closure class

<?php
/*
 *
 */
namespace MyNamespace;

class closure {
    public $priority = 10;
    public $method;
    public $args = 1;
    public $hook;
    public $persistent = false;
    public $hookType;

    const TYPE_ACTION = 1;
    const TYPE_FILTER = 2;

    // Instantiators
    public static function createFilter($hook, $method, $priority=10, $args=1, $persistent=false, $hookType=self::TYPE_FILTER){
        $closure = new static();
        if(is_callable($method)) {
            $closure->hookType = $hookType;
            $closure->hook = $hook;
            $closure->method = $method;
            $closure->priority = $priority;
            $closure->args = $args;
            $closure->persistent = $persistent;
            static::apply($closure);
        } else {
            $closure = false;
        }
        return $closure;
    }

    public static function createAction($hook, $method, $priority=10, $args=1, $persistent=false){
        return static::createFilter($hook, $method, $priority, $args, $persistent, self::TYPE_ACTION);
    }

    public static function createPersistentFilter($hook, $method, $priority=10, $args=1){
        return static::createFilter($hook, $method, $priority, $args, true);
    }

    public static function createPersistentAction($hook, $method, $priority=10, $args=1){
        return static::createAction($hook, $method, $priority, $args, true);
    }

    // public API [WordPress needs public visibility]
    public function filter(){
        $args = func_get_args();
        $val = $args[0];
        if(is_callable($this->method)){
            $val = call_user_func_array($this->method, $args);
        }
        if(!$this->persistent) {
            static::remove($this);
        }
        if($this->hookType == self::TYPE_FILTER) {
            return $val;
        }
        return true;
    }

    // Utility methods
    public static function apply(closure $closure){
        if($closure->hookType == self::TYPE_ACTION){
            add_action($closure->hook, array($closure, 'filter'), $closure->priority, $closure->args);
        } else {
            add_filter($closure->hook, array($closure, 'filter'), $closure->priority, $closure->args);
        }
    }

    public static function remove(closure $closure){
        if($closure->hookType == self::TYPE_ACTION){
            remove_action($closure->hook, array($closure, 'filter'), $closure->priority);
        } else {
            remove_filter($closure->hook, array($closure, 'filter'), $closure->priority);
        }

    }
}

As long as it's created using the createFilter or createAction instantiation methods, then by default, a closure object will remove it's own hook the first time it's employed. You can use the persistent methods if you don't want it to do that, but as I'm not envisaging using this for every single action and filter, only for ones like my posts_where where I need it removed following its first application, then I'm happy for that to be the default.

Here's an example of the closure class in use:

use MyNamespace\closure;

$mySpecialVar = 4077;
closure::createFilter('posts_where', function($where) use ($mySpecialVar) {
  $currentWhere = preg_replace("'^\s?AND\s'i", '', $where);
  $where = ' AND wp_posts.ID IN (SELECT 
     DISTINCT mp_a.meta_value
     FROM wp_postmeta mp_a
     WHERE mp_a.meta_key = \'my_meta_key_im_looking_for\'
       AND mp_a.post_id = '.(1*$mySpecialVar).') 
     OR (' . $currentWhere . ') ';
  return $where;
});

If I needed to I could still store the closure object as a variable and use it on multiple filters. In fact I could probably add a method to the class to accept multiple filter strings to keep everything together.

Because the closure object remembers its own arguments, should you retain a copy of the closure you may at any time use the method:

closure::remove($myClosure);

to remove the hook. Changing the priority in the original closure creation won't now affect the removal.

Top comments (0)