Hacking Hooks – Bending WordPress Hooks and Filters to Your Will
Written by Bunkers on March 4, 2017
When I was setting up my writing environment for this blog, I went on a hunt for a good Markdown plugin to use for WordPress. I tried a number of different ones, but I wanted something simple, that rendered the Markdown on the server, and stored my content as Markdown.
After some digging around and trying out a handful of different ones, I settled on Typewriter. The plugin is without an update for years, which is slightly concerning, but it had the features I needed - stripping out the visual editor, storing the content as Markdown and using a PHP Markdown parser to render the content on the server.
Although Typewriter appeared to have all the features I needed, I quickly realised that the default Markdown parser that it used, Michelf's PHP-Markdown, didn't have support for GitHub syntax, meaning that I couldn't indicate the language on code blocks, amongst other things.
After some research I came across Parsedown which is a good alternative PHP parser and supports the GitHub syntax I was after. So, I set about writing a WordPress plugin that extends Typewriter but converting it to use Parsedown.
To begin with I thought this would be straightforward:
- Find the function used to translate the Markdown.
- Write a comparable function that renders the Markdown with Parsedown
- Remove the original function's registration to any WordPress hooks and filters
- Register our new function against these same hooks and filters
The first step was easy. Looking at the Typewriter source code, the plugin consists of a single class. Registered against the 'the_content'
and 'the_excerpt'
filters is a method called do_markdown
.
The next step was also easy. The do_markdown
method took in the Markdown and returned the rendered HTML. Thankfully the Parsedown library works in a similar way to the PHP Markdown library used by Typewriter. The equivalent function therefore is:
function do_typewriter_parsedown($content){ $parse = new Parsedown(); return $parse->text($content); } |
The next step however, is where it gets tricky. WordPress plugin writing etiquette says that if you are registering instance methods of a class to filters like this, you should provide a mechanism for getting that instance, like a global variable storing or static method. This is specifically to help with extensibility.
Unfortunately the last line of the Typewriter plugin was just:
new Dev7Typewriter(); |
And no static field in the class that referenced itself. So how do we get at this object instance so we can remove the registered filter callbacks?
All is not lost. It just means we need to search through the global WordPress filters array until we find a filter registered with an instance of the Dev7Typewriter
class. We can then use that instance to remove the filters currently registered. Then we're free to register our new function defined above.
The global WordPress filters object allows indexing based on the filter name. This returns a WPFilter object that has a method to retrieve all the callbacks with a given priority. By default when you register a filter it's given a priority of 10, so to retrieve all the callbacks with this default priority registered against the 'the_content'
filter we can do something like:
global $wp_filter; $filters=$wp_filter['the_content']->callbacks[10]; |
Now $filters
will be an associative array where the key is unique but contains the name of the function and the value is another associative array with a key of 'function'
. If this is a function on a class then the value of the 'function'
key will also be an array consisting of two elements. The object instance it's registered against, and the method name.
This means that the next bit of our function looks like this:
foreach ($filters as $filterid => $value){ if(strpos($filterid, 'do_markdown') !== false && is_array($value['function']) && is_a($value['function'][0],'Dev7Typewriter')){ ... } } |
This loop goes through our callbacks, looks for a key that contains our method name 'do_markdown'
which has a value of an array with the key of 'function'
, which in turn is an array whose first element is an object that is an instance of 'Dev7Typewriter'
, phew!
That's a bit complex, but we're now free to remove these callbacks and add in our own.
Our full function to remove the callbacks looks like this:
function remove_do_markdown(){ global $wp_filter; $filters=$wp_filter['the_content']->callbacks[10]; foreach ($filters as $filterid => $value){ if(strpos($filterid, 'do_markdown') !== false && is_array($value['function']) && is_a($value['function'][0],'Dev7Typewriter')){ remove_filter('the_content',array($value['function'][0],'do_markdown')); remove_filter('the_excerpt',array($value['function'][0],'do_markdown')); } } } add_action('plugins_loaded','remove_do_markdown'); |
On finding the correct callbacks we should probably break out of the loop, so there's room for improvement here. Lastly we register our new function against the same filter names:
add_filter('the_content','do_typewriter_parsedown',0); add_filter('the_excerpt','do_typewriter_parsedown',0); |
The reason for the 0 (i.e. the highest) priority will become clear when I discuss the integration with WP Syntax. But for now we've successfully managed to extend the typewriter plugin. The more you can reuse plugins rather than reinvent the wheel, the better. This could break if the Typewriter plugin updates, but I think fixing that and building on its functionality is better than the alternative, which would be to fork the code.