Using WordPress plugins as Symbolic Links

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
Download SymbolicPress on GitHub

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.

When to Use Symbolic Links

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.

Liked this article? Sharing is caring!

13 Comments on this subject

  1. Simon said:

    Why not use git? You could have a local bare git repo where you push the changes and have a post hook that pulls into the installs, when doing it manually is too much work.

    You could even use different branches for when you are doing some wp version specific bug fixing on the plugin.

    • Gaya said:

      That would mean I have to push all the changes before I can test the plugin on different installs right? That’s not what I am trying to accomplish.

      Git could be a great solution to deploy your plugin over multiple installs after testing though. Would make a great follow up post :)

      • Simon said:

        Well, yeah, that is the point of creating a “fix” of “feature” branch. Pushing isn’t that much work (you are already committing changes) and you’ll get to have “real” installs on each WP base. You can do the pulls on each install automatically so it really is just one button.

        As i said, it has the added benefit that when your plugin works on all installs but one, you can work on that one install without having to fear you’ll break the others. Just a workflow thing ;-)

        I get the point of symbolic linking, just that i feel that there are git workflows that are designed for this and it doesn’t requite you to add code that doesn’t make sense after you are done testing.

  2. Sarah said:

    Well, this is really great. My dev team run into this kind of problem recently, and they have to install the plugin on multiple instances manually to make sure the plug-in works fine on various deploying environment. Huge time-saving ! Thanks, Gaya !

    • Gaya said:

      Good question. It will not break the plugin if you move the source to the “real” plugins folder. So you can leave Symbolic Press in there.
      You can go about and rewrite it to the “normal way” by removing Symbolic link if you want.

  3. Alex said:

    I’m getting this error in my console error log when updating a plugin:

    [Sat Dec 21 11:00:23 2013] [error] [client ::1] Request exceeded the limit of 10 internal redirects due to probable configuration error. Use ‘LimitInternalRecursion’ to increase the limit if necessary. Use ‘LogLevel debug’ to get a backtrace., referer: http://localhost/test/wp-admin/update.php?action=upgrade-plugin&plugin=posts-to-posts%2Fposts-to-posts.php&_wpnonce=ab36514012

  4. Aaron said:

    Thanks for sharing. Very naively thought I could develop plugins as submodules inside of a parent repo. Yeah…don’t ever do that. It only works in Git books.

Leave your reply

Your email address will not be published. Avatars through Gravatar.

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>