Using WordPress plugins as Symbolic Links

Download SymbolicPress on GitHub

If you want to use a single plugin on different WordPress installs you might want to go for a symlinked plugin folder. That way we can have one codebase and apply it on multiple installs.

That would be great right? Sadly, WordPress doesn’t support this (yet). In this post I’ll explain how I got WordPress to accept my plugins as symbolic links.

Using WordPress plugins as Symbolic Links

The Problem

There are a few functions we use in WordPress when it comes to plugin development. The ones that are causing the problems we run into when using symbolic links are plugins_url, plugin_basename, register_activation_hook, register_deactivation_hook and register_uninstall_hook.

These functions break because they assume all the plugin files are physically in the plugin folder defined in WordPress. It will remove the path to the WordPress plugins directory from the file path we pass to above functions. __FILE__ is in a different folder physically on your system when you’re using symbolic links.

In the future we’ll surely see this solved in the core. You can follow ticket #16953 on Trac to see it’s progress. For now, you can use my solution.

Plugin development in WordPress can be a pain when it comes to testing. I like to have a single place to work on my plugin, but testing it on multiple installs at the same time.

Using symlinks can be the solution when you want to use one codebase and use it in different places.

In this case we’ve got a plugin in /Users/gaya/Git/awesome-plugin/ and want it available in /Users/gaya/Sites/wordpress-test/wp-content/plugins/

Open up terminal and run the following command (Mac OSX and Unix):

ln -s /User/gaya/Git/awesome-plugin/ /Users/gaya/Sites/wordpress-test/wp-content/plugins/awesome-plugin

Note that the target path has no trailing slash. Also change the paths to your situation.

The Solution: Symbolic Press

Download the Symbolic Press helper class on GitHub and place it somewhere in your plugin directory. I like to keep my external classes in a seperate folder from my own classes. Save the file as class-symbolic-press.php.

In this case my plugin is called awesome-plugin so I can assume a file called awesome-plugin.php is in this directory.

Include the Symbolic Press class in this file and create a new instance of the class passing __FILE__ as a parameter like so:

include "libs/class-symbolic-press.php";
new Symbolic_Press(__FILE__);

Every time you use plugins_url() it won’t include the path to your symlinked plugin folder. So first problem fixed!

What it does is this:

add_filter( 'plugins_url', 'plugins_symbolic_filter' );

public function plugins_symbolic_filter( $url ) {
    //set the path to the plugin file
    $path = dirname( $this->plugin_path );

    //get the basename of the path
    $basename = basename( $path );

    //check if this plugin is in the basename that is checked
    if ( preg_match( '/' . $this->plugin_name . '$/', $basename ) ) {
        $path = dirname( $path );
    }

    return str_replace( $path, "", $url );
}

It will check if the plugin name is found in the path and remove the file path (which won’t be removed by WordPress) from the URL. So now your assets will work again.

What about registration, deregistration and uninstall hooks?

Registration hooks in WordPress bind actions on it’s filenames. Since it uses the function plugin_basename it will bind it’s action on a filename which doesn’t exist.

What happens is that plugin_basename only removes the WP_PLUGIN_DIR and WPMU_PLUGIN_DIR from the file path, but since our plugin is not physically in either folders the path wont get stripped.

For this I created a small function in Symbolic Press: plugin_basename. Which can be called by using Symbolic_Press::plugin_basename(). Easy peasy.

The solution is a copy of the original plugin_basename but with two lines added before the preg_replace and a replacement for the preg_replace:

$sp_plugin_dir = dirname( dirname( $sp_plugin_dir ) );
$sp_plugin_dir = preg_replace( '|/+|', '/', $sp_plugin_dir ); // remove any duplicate slash
$file          = preg_replace( '#^' . preg_quote( $sp_plugin_dir, '#' ) . '/|^' . preg_quote( $plugin_dir, '#' ) . '/|^' . preg_quote( $mu_plugin_dir, '#' ) . '/#', '', $file ); // get relative path from plugins dir

How do I use the registration, deregistration and uninstall hooks?

When you’re using a registration or deregistration hook WordPress does no more than:

add_action( 'activate_awesome-plugin/awesome-plugin.php', $function );

For this you can use:

$plugin_basename = Symbolic_Press::plugin_basename( $file );

//bind the activation action
add_action( 'activate_' . $plugin_basename, $function );

Or the respectable static functions you can use to register these hooks.

Symbolic_Press::register_activation_hook( $filepath, $function ); Symbolic_Press::register_deactivation_hook( $filepath, $function );
Symbolic_Press::register_uninstall_hook( $filepath, $function );

Conclusion

It’s a workaround, but it works very good. Now you can go and test your plugins agains multiple installs without any problems.

You can also create a single place to update and maintain your plugins on your production environment. That might get a bit tricky, but that’s a completely different story.

Let me know what you think in the comments.