Contenu principal

Les extensions (et thèmes) symlinkés dans WordPress

WordPress 3.9 a introduit une nouveauté bien pratique pour les développeurs : nous pouvons désormais « symlinker » des extensions sans risquer de casser une quelconque compatibilité.

Quelle en est l’utilité ?

Je pense principalement aux gens comme moi, qui ont plusieurs installations WordPress sur un serveur local : des extensions en développement peuvent être regroupées dans un dossier commun et partagées vers plusieurs sites. On retrouve en quelque sorte le système multi-site de WordPress. L’intérêt ? Vous développez une extension et voulez la tester sur plusieurs configurations : vous avez une seule copie et ne vous perdez plus pour savoir où a été faite la dernière modification. En somme, l’extension sera identique en permanence sur tous les sites.

J’ai testé ça avant-hier soir et j’avoue être conquis, ça va m’éviter pas mal de prises de têtes et d’erreurs. Ainsi, j’ai créé un dossier wp-symlinks/plugins/ au même niveau que mes sites et y ai mis pas mal d’extensions que je développe.

Cependant, je me suis rendu compte plus tard d’un gros point noir à utiliser des extensions symlinkées. Je comptais mettre dans mon dossier partagé mes outils de développement (Debug Bar, Adminbar Tools, etc), des extensions du repository que j’utilise souvent, etc. C’est également possible, mais… il ne faudra pas tenter de les mettre à jour depuis WordPress : WordPress n’y arrivera simplement pas et détruira l’extension (pas le lien symbolique) :(.
MAIS, il y a tout de même deux solutions envisageables :

  1. Mettre à jour les extensions à la mano dans le vrai dossier.
  2. Installer ces extensions dans… une installation WordPress. Et faire les mises à jour depuis ce site. :) (pas encore testé)

Créer un lien symbolique

Mais d’abord un petit rappel, comment créer un lien symbolique. J’avoue être allergique à la ligne de commande, principalement parce que c’est un langage que je n’ai pas envie d’apprendre, mais là, pas le choix. (pour les windoziens je vous laisse chercher car je ne peux tester, donc plutôt que de vous dire une bêtise…)

ln -s chemin-reel-du-plugin chemin-du-symlink-dans-le-site

Concrètement ça a donné ça pour moi :

ln -s /Users/SuperPoney/Sites/wp-symlinks/plugins/debug-bar /Users/SuperPoney/Sites/mon-site/wp-content/plugins/debug-bar


Ha oui, détail, avant de créer le lien symbolique il faut d’abord déplacer les extensions dans le « dossier commun ». #JustSayin’

A noter que vous pouvez également symlinker le dossier plugins lui-même afin de dupliquer toutes les extensions d’un coup. Ça n’a pas été mon choix car je ne désire pas avoir les mêmes extensions partout. Du coup, je garde mes lignes de commande dans un coin et je les copie/colle au besoin pour chaque nouveau site.

Bon, et ça donne quoi ? Si vous regardez la capture d’écran vous remarquerez les petites flèches au niveau des dossiers des extensions. Si je double-clic dessus, je me retrouve dans le vrai dossier.

Concrètement, ça change quoi dans nos extensions ? À priori rien. Il faut juste être conscient de deux choses :

  1. __FILE__ ne contiendra pas WP_PLUGIN_DIR car __FILE__ résout les symlinks, il contiendra donc le chemin réel vers le plugin.
  2. plugin_dir_url() contiendra WP_PLUGIN_URL, c’est à dire l’url du dossier des plugins du site (c’est ça la nouveauté de WordPress 3.9 en fait).

Les thèmes symlinkés, ça marche ?

Autre point également, les liens symboliques fonctionnent aussi pour les thèmes \o/
Je pense même que cela marchait avant la 3.9.
Même chose, il n’y a rien à faire, vous pourrez utiliser get_template_directory(), get_template_directory_uri(), get_stylesheet_directory() et get_stylesheet_directory_uri() sans soucis. La seule différence par rapport aux extensions symlinkées, c’est que get_template_directory() et get_stylesheet_directory() retournent les chemins des liens symboliques, donc comme si le thème était bien dans le site.

MU-Plugins

Allez, un dernier pour la route : on peut faire la même chose avec les extensions Must-Use, mais il faut bidouiller.
Il y a déjà une limite avec ces liens symboliques, que je n’ai pas encore annoncée (ni testée ^^) : il n’est pas possible de symlinker un plugin qui n’a pas son propre dossier, comme Hello Dolly par exemple. Du coup on est bien embêtés pour les MU-Plugins, étant donné que si on les met dans un dossier, WordPress ne les prendra pas en compte.
Pour résoudre ce soucis il faut créer un loader. C’est à dire que l’on va mettre chaque MU-Plugin dans son propre dossier. Puis un MU-Plugin, dit « loader », va se charger d’inclure tous les MU-Plugins.
Pas assez clair ? Regardez ma capture d’écran, il y a un MU-Plugin mu-plugins-loader.php directement dans le dossier mu-plugins, alors que mes deux autres MU-Plugins sont dans leur propre dossier. La technique est décrite dans l’article de « Make Core » en lien plus haut. Voici à quoi ressemble mon loader :

01020304050607080910111213141516171819202122232425262728293031323334353637383940414243444546

<?php
/*
 * Plugin Name: MU Plugins loader
 * Description: Allow to load MU Plugins from sub-directories.
 * Version: 1.0
 * Author: Grégory Viguier
 * Author URI: https://www.screenfeed.fr/greg/
 * License: GPLv3
 * License URI: https://www.screenfeed.fr/gpl-v3.txt
*/


if( !defined( 'ABSPATH' ) )
	die( 'Cheatin\' uh?' );

function sfmp_loader() {
	// Don't run twice or more.
	static $done = false;
	if ( $done ) {
		return;
	}

	$done    = true;
	$dirname = dirname( __FILE__ );
	$plugins = array(
		'mash-basics/mash-basics.php',
		'cron-auth/cron-auth.php',
	);

	foreach ( $plugins as $plugin ) {
		$path = $dirname . '/' . $plugin;

		if ( ! file_exists( $path ) ) {
			continue;
		}

		if ( function_exists('wp_register_plugin_realpath') ) {
			// Ensure mu-plugins subdirectories can be symlinked in WP 3.9+
			wp_register_plugin_realpath( $path );
		}

		include( $path );
	}
}

sfmp_loader();

Le tout est d’utiliser la fonction wp_register_plugin_realpath() qui va se charger en gros de stocker les chemins de vos MU-Plugins (le réel et celui du symlink), qui seront ensuite utilisés dans plugin_basename(), elle-même utilisée dans plugins_url(), et elle-même utilisée dans plugin_dir_url().
Le petit désagrément du loader c’est que les MU-Plugins inclus n’apparaissent plus dans la liste de l’administration.

Mais ce n’est pas fini, nous avons tout de même un problème maintenant. Regardons d’un peu plus près les fonctions suivantes :

010203040506070809101112131415

function plugin_dir_url( $file ) {
	return trailingslashit( plugins_url( '', $file ) );
}

function plugins_url($path = '', $plugin = '') {
	// ...
	$mu_plugin_dir = str_replace( '\\' ,'/', WPMU_PLUGIN_DIR ); 
	$mu_plugin_dir = preg_replace( '|/+|', '/', $mu_plugin_dir );

	if ( !empty($plugin) && 0 === strpos($plugin, $mu_plugin_dir) )
		$url = WPMU_PLUGIN_URL;
	else
		$url = WP_PLUGIN_URL;
	// ...
}

Habituellement ce que je fais, et vous aussi peut-être, c’est ça : plugin_dir_url( __FILE__ ) pour avoir l’url du dossier de mon extension. Sauf que là, ça ne va pas marcher pour un MU-Plugin symlinké. Il fait regarder le if / else de plugins_url() : si WPMU_PLUGIN_DIR n’est pas trouvé dans le chemin de l’extension, on utilise alors WP_PLUGIN_URL pour construire l’url. Or, on a vu que __FILE__ résout les liens symboliques. Autant ça ne gène pas pour une extension classique, autant là ça pose problème.

On peut résoudre ce problème de deux manières : tenir compte de cette possibilité lorsque je développe mes MU-Plugins (mais peut-être que tous ne sont pas de moi), utiliser un filtre pour corriger le « bug » pour tous les MU-Plugins.

1- Ajuster le chemin de mon MU-Plugin
plugins_url() se base sur le chemin que je lui donne, je peux donc m’arranger pour lui fournir ce qu’il faut. Voici comment définir le chemin vers le dossier de mon extension, en s’adaptant à n’importe quelle situation.

0102030405060708091011121314151617181920212223

global $wp_plugin_paths;
$wpmu_plugin_dir	= str_replace( '\\', '/', WPMU_PLUGIN_DIR );
$wpmu_plugin_dir	= preg_replace( '|/+|','/', $wpmu_plugin_dir );
$my_plugin_file		= str_replace( '\\', '/', __FILE__ );
$my_plugin_file		= preg_replace( '|/+|','/', $my_plugin_file );

// MU Plugin
if ( $wpmu_plugin_dir && strpos( strtolower( $my_plugin_file ), strtolower( $wpmu_plugin_dir ) ) === 0 ) {

	define( 'MY_PLUGIN_DIR', WPMU_PLUGIN_DIR . '/my-plugin/' );
}
// MU Plugin symlinked
elseif ( ! empty( $wp_plugin_paths ) && is_array( $wp_plugin_paths ) && false !== ($symlink_path = array_search( dirname($my_plugin_file), $wp_plugin_paths )) ) {

	define( 'MY_PLUGIN_DIR', $symlink_path . '/' );
}
// Plugin
else {

	define( 'MY_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
}

define( 'MY_PLUGIN_URL', plugin_dir_url( MY_PLUGIN_DIR . 'index.php' ) );

Avec ceci, votre extension peut être :

  1. Un MU-Plugin,
  2. Un MU-Plugin dans un dossier, et inclus avec un loader,
  3. Un MU-Plugin symlinké,
  4. Une extension classique, symlinkée ou non.

2- créer un filtre qui va corriger l’url pour tous les MU-Plugins
Ici, l’idée est de filtrer ‘plugins_url’. Ce filtre trouverait bien sa place dans le loader vu précédemment, juste avant la fonction sfmp_loader(). Cependant je trouve la solution un poil lourde car elle implique de faire un foreach à chaque fois que plugins_url() est appelé. Bon, c’est pas bien méchant non plus, et les symlinks étant plutôt destiné à un environnement de développement (à mon avis), l’argument des performances est secondaire.

01020304050607080910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576

<?php
/*
 * Plugin Name: MU Plugins loader
 * Description: Allow to load MU Plugins from sub-directories.
 * Version: 1.0
 * Author: Grégory Viguier
 * Author URI: https://www.screenfeed.fr/greg/
 * License: GPLv3
 * License URI: https://www.screenfeed.fr/gpl-v3.txt
*/


if( !defined( 'ABSPATH' ) )
	die( 'Cheatin\' uh?' );


add_filter( 'plugins_url', 'plugins_url_for_wpmu_plugins', 10, 3 );

function plugins_url_for_wpmu_plugins( $url, $path, $plugin ) {
	global $wp_plugin_paths;

	if ( ! empty( $wp_plugin_paths ) && is_array( $wp_plugin_paths ) && 0 !== strpos( $url, WPMU_PLUGIN_URL ) ) {
		$symlink_path = false;

		foreach ( $wp_plugin_paths as $s_path => $wp_plugin_path ) {
			if ( 0 === strpos( $plugin, $wp_plugin_path ) ) {
				$symlink_path = $s_path;
				break;
			}
		}

		if ( $symlink_path ) {
			$mu_plugin_dir = wp_normalize_path( WPMU_PLUGIN_DIR );

			if ( 0 === strpos( $symlink_path, $mu_plugin_dir ) ) {
				$plugin = str_replace( $wp_plugin_paths[ $symlink_path ], $symlink_path, $plugin );
				return plugins_url( $path, $plugin );
			}
		}
	}

	return $url;
}


function sfmp_loader() {
	// Don't run twice or more.
	static $done = false;
	if ( $done ) {
		return;
	}

	$done    = true;
	$dirname = dirname( __FILE__ );
	$plugins = array(
		'mash-basics/mash-basics.php',
		'cron-auth/cron-auth.php',
	);

	foreach ( $plugins as $plugin ) {
		$path = $dirname . '/' . $plugin;

		if ( ! file_exists( $path ) ) {
			continue;
		}

		if ( function_exists('wp_register_plugin_realpath') ) {
			// Ensure mu-plugins subdirectories can be symlinked in WP 3.9+
			wp_register_plugin_realpath( $path );
		}

		include( $path );
	}
}

sfmp_loader();

Pour info, la globale $wp_plugin_paths (encore une) est utilisée par wp_register_plugin_realpath(), c’est elle qui stocke les chemins réel/symlink des extensions symlinkées.

Conclusion

Si pour les extensions Must-Use des efforts restent à faire, les extensions classiques et les thèmes peuvent profiter des liens symboliques sans problème.
À mon sens, tout ce qui manquerait c’est une fonction comme plugin_dir_path() qui retournerait le chemin symlinké au lieu du vrai chemin, afin d’être tout à fait transparent lors du développement d’extensions. Le support des extensions Must-Use serait également un atout.

Ne pas oublier aussi qu’il ne faut pas tenter de mettre à jour une extension ou un thème symlinké. Peut-être faudrait-il d’ailleurs prévoir un avertissement. En plus si vous avez activé les mises à jour automatiques des extensions… x)

En attendant, amusez-vous bien :)
See ya!