13 июля 2012

Wordpress Plugin длиной в одну страницу

WordPressPHPJavaScript
Почему люди любят Wordpress? Потому что с ним просто работать. В нём нет гибкости большущих CMS вроде Joomla и Drupal, — а значит, не запутаешься. И ещё он очень популярен — а значит, можно найти плагины на все случаи жизни.

Неспроста несмотря на осуждение со стороны Lurkmore.ru, Wordpress-ом пользуются и Герб Саттер, и Марк Шаттлворт, и много кто ещё. Например, я.

В своих записях я очень часто ссылаюсь на чужие блоги. И мне пришла идея — а почему бы не показывать рядом с ником человека, на которого я ссылаюсь, ещё и значок его сервиса? Например, птичку из твиттера или букву B из блогспота? Похожий функционал есть, например, в Википедии, да и многие блогохостинги это позволяют (например, Dreamwidth).

Так и родился плагин для Wordpress Rikki's WP Social Icons. Позволяет за один клик мышкой добавить ссылку на эккаунт в каком-нибудь сервисе, от социальной сети до GitHub.

Зачем?


Есть много статей о том, как правильно писать plugin-ы для Wordpress, но не в одной из них не было ни слова про те баги, с которыми я столкнулся в процессе работы. Поэтому и записываю свои впечатления — вдруг кто-то столкнётся с тем же, а как решить — не знает. И указываю все доработки, которые обычно опускают в учебных примерах.

Во время работы я довольно много пользовался исходниками чужих плагинов, а также наработками Jenyay (ссылки на его цикл статей приведён в приложении). Но «кустарной» реализации было мало. Plugin тем и удобен, что каждый конечный пользователь может доработать его сам. А значит, нужно сделать так, чтобы добавить к нему поддержку нового сервиса было делом пяти минут и одной переустановки.

Особенно помогало работе то, что я собирался использовать этот плагин сам (т.е., питаться собачьим кормом, как говорят наши американские коллеги). И именно мои мучения с первыми набросками подсказали мне, где и что можно улучшить.

Программируем


Проектируем


Начнём с того, что писать плагины для Wordpress не просто, а очень просто. Не нужно создавать ни классов, от чего-то там унаследованных, ни мудрить с форматами, ни следовать каким-то хитрым спецификациям. «События» вынесены в специальные объекты, которые работают примерно как event-ы в «больших» приложениях.

Для начала надо хорошенько обдумать, что нам нужно. А нужен нам способ маркировки отдельных слов, который был бы виден при редактировании, и автоматически переделывался в ссылку в постах, страницах и RSS.

Очень похоже работает shortcode — специальный тег, окружённый квадратными скобками и специально заточенные под обработку плагинами. Они появились ещё версии 2.5, которая вышла в далёком 2008 году, так особые проблемы с совместимостью нам не грозят.

Какой формат нам следует принимать от пользователя? Т.к. конечный HTML-код будет отличаться только иконкой и url-ом, было бы логично завести один shortcode и одну функцию, которая бы его обрабатывала. Дополнительные сведения можно передавать и через параметры, что очень удобно. К тому же, чем меньше shortcode-ов мы создадим, тем меньше шанс, что мы вступим в конфликт с каким-то другим плагином.

Мой shortcode выглядел так:

[userid]

Его параметры:

'id' — необязательный параметр. id, под которым наш герой зарегистрирован на сервисе. Например. torvalds-family.

'type'- сервис

'url'- необязательный параметр. url на который мы хотим сослаться (например, профиль или какой-то отдельный пост в блоге).

А использовать его вот так:

[userid type="blogspot" id="torvalds-family"]Linus Torvalds[/userid]


или так:

[userid type="blogspot" url="http://blogspot.com"]blogspot[/userid]


Пишем ядро



Теперь создаём структуру для нашего плагина (см. на GitHub) и набрасываем в rikkis-wp-social-icons.php наше миниатюрное ядро:

class socialusers
{
	var $options = array(
		"blogspot" => "http://%s.blogspot.com/",
		"ljuser" => "http://%s.livejournal.com/",
		"ljcomm" => "http://livejournal.com/community/%s",
		"liruboy" => "http://www.liveinternet.ru/users/%s/",
		"lirugirl" => "http://www.liveinternet.ru/users/%s/",
		"vk" => "http://vk.com/%s",
		"twitter" => "http://twitter.com/#!/%s/",
		"facebook" => "http://www.facebook.com/%s",
		"google_plus" => "https://plus.google.com/%s",
		"wordpress" => "http://%s.wordpress.com/",
		"habrahabr" => "http://%s.habrahabr.ru/",
		"github" => "http://github.com/users/%s/"
	);
	function socialusers(){
		if (!function_exists ('add_shortcode') ) return;
		add_shortcode('userid', array (&$this, 'icon_func') );
	}
	function icon_func($atts, $content="") {
		if (!$content)	return "";
		extract( shortcode_atts ( array('id' => null, 'type' => null, 'url' => null), $atts ) );
		if (!$type || !array_key_exists($type, $this->options) )	return $content;
		if (!$id)	$id = $content;
		$userinfo_url = esc_url(($url) ? $url : sprintf($this->options[$type], trim($id)));
		$userpic_url = esc_url(plugins_url( "js/img/$type.gif" , __FILE__ ));
		return "<span style='white-space: nowrap; display: inline !important;'><a href='$userinfo_url' ref='nofollow'><img src='$userpic_url' alt='[info]' width='17' height='17' style='vertical-align: bottom; border: 0; padding-right: 1px;vertical-align:middle; margin-left: 0; margin-top: 0; margin-right: 0; margin-bottom: 0;' /></a><a href='$userinfo_url' ref='nofollow'><b>$content</b></a></span>";
	}
}
$socialusers = new socialusers();


Чтобы избежать конфликта имён переменных, мы сложили все наши вызовы в один класс socialusers. В переменной options хранятся id и URL-ы сервисов, на которые мы собираемся ссылаться, с заменой имени пользователя на %s. В папке js/img кладём gif-ки с соответствующими иконками.

В конструкторе мы проверяем, поддерживает ли Wordpress shortcode, и, если нет, то не делаем уже ничего. Нехорошо обваливать пользователю весь Wordpress только потому, что он пользуется старой версией.

Если всё в порядке — мы добавляем новый shortcode, указывая для него в качестве обработчика функцию icon_func из этого же экземпляра класса.

icon_func — она очень короткая и очень интересная. В неё приходит 2 параметра:

$atts — массив атрибутов
$content — текст между тегами

Пользователь увидит вместо shortcode то, что вернёт ему эта функция. Именно здесь (в последнем return) и формируется окончательный код нашего блока.

По совету из комментариев, все пришедшие «на вход» строки проверяются через esc_url, чтобы не было дырок в безопасности.

Заслуживают внимания две строчки:
extract( shortcode_atts ( array('id' => null, 'type' => null, 'url' => null), $atts ) );
</<source>

формирует из элементов массива $atts локальные переменные с соответствующими именами
и
<source lang="php">
plugins_url( "js/img/$type.gif" , __FILE__ );


Получает url иконки по типу, относительно текущей директории. Именно для этого нужен атрибут __FILE__. Если же его нет (а авторы некоторых плагинов и примеров явно про него не слышали), то придётся вставлять имя директории плагина и всё равно это может не заработать.

Не менее важно сделать trim() для $id. Дело в том, что если дважды щёлкнуть по слову в editor-е Wordpress-а, то он выделит слово вместе с пробелом после него. В результате мы получим имя учётной записи с совершенно неуместным пробелом и ссылка работать не будет.

В принципе, уже в таком виде (17 строк кода + 14 строк настроек) наш плагин можно ставить и использовать. Настоящий хакер презирает оконные интерфейсы :). Вы можете попробовать запаковать php и картинки в zip и установить их в wordpress, как устанавливают обычный плагин.

Очень рекомендую поставить для подобных экспериментов локальный Apache+PHP+mySQL, и к ним впридачу Wordpress. Отладка пойдёт намного веселее — например, вместо установки-переустановки можно будет просто подменять файлы в соответствующем каталоге.

Добавляем кнопки



За редактирование постов и страниц в Wordpress отвечает отдельный компонент tinyMCE. Чтобы его дёрнуть из основного PHP, нужно немного расширить нашу первоначальную форму:

class socialusers
{
	var $options = array(
		"blogspot" => "http://%s.blogspot.com/",
		"ljuser" => "http://%s.livejournal.com/",
		"ljcomm" => "http://livejournal.com/community/%s",
		"liruboy" => "http://www.liveinternet.ru/users/%s/",
		"lirugirl" => "http://www.liveinternet.ru/users/%s/",
		"vk" => "http://vk.com/%s",
		"twitter" => "http://twitter.com/#!/%s/",
		"facebook" => "http://www.facebook.com/%s",
		"google_plus" => "https://plus.google.com/%s",
		"wordpress" => "http://%s.wordpress.com/",
		"habrahabr" => "http://%s.habrahabr.ru/",
		"github" => "http://github.com/users/%s/"
	);
	function socialusers(){
		if (!function_exists ('add_shortcode') ) return;
		add_shortcode('userid', array (&$this, 'icon_func') );
		add_filter( 'mce_buttons_3', array(&$this, 'mce_buttons') );
		add_filter( 'mce_external_plugins', array(&$this, 'mce_external_plugins') );
	}
	function icon_func($atts, $content="") {
		if (!$content)
			return "";
		extract( shortcode_atts ( array('id' => null, 'type' => null, 'url' => null), $atts ) );
		if (!$type || !array_key_exists($type, $this->options) )
			return $content;
		if (!$id)
			$id = $content;
		$userinfo_url = esc_url(($url) ? $url : sprintf($this->options[$type], trim($id)));
		$userpic_url = esc_url(plugins_url( "js/img/$type.gif" , __FILE__ ));
		return "<span style='white-space: nowrap; display: inline !important;'><a href='$userinfo_url' ref='nofollow'><img src='$userpic_url' alt='[info]' width='17' height='17' style='vertical-align: bottom; border: 0; padding-right: 1px;vertical-align:middle; margin-left: 0; margin-top: 0; margin-right: 0; margin-bottom: 0;' /></a><a href='$userinfo_url' ref='nofollow'><b>$content</b></a></span>";
	}
	function mce_external_plugins($plugin_array) {
		$plugin_array['rikkisocialicons'] = plugins_url ('js/rikkis-wp-social-icons-editor_plugin.js', __FILE__ );
		return $plugin_array;
	}
	function mce_buttons($buttons) {
		return array_merge($buttons, array_keys($this->options));
	}
}
$socialusers = new socialusers();


Тут всё просто — mce_external_plugins подгружает JavaScript, в котором и будут генерировать кнопки, а mce_buttons кладёт туда все ключи из словаря options.

Теперь нам предстоит написать JavaScript для кнопочек. Увы, но tinyMCE вшит в Wordpress настолько прочно, что стандартный способ передачи параметров из PHP в JavaScript для плагинов Wordpress здесь не сработает. Конечно, можно было бы попытаться получить их через JSon и добиться, чтобы исправление нужно было вносить действительно только в одном месте. Но это тот самый случай, когда малозначительное удобство может вызвать значительные проблемы.

Обрамление у tinyMCE плагина довольно стандартное:

(function() {
	tinymce.create('tinymce.plugins.RikkiSocialIconsPlugin', {
		init : function(ed, url) {
			//здесь добавляем кнопки
			}
		},
		getInfo : function() {
			return {
				//кто виноват в том, что всё это сделал
			};
		}
	});
	tinymce.PluginManager.add('rikkisocialicons', tinymce.plugins.RikkiSocialIconsPlugin);
})();


Добавление одной кнопки, которая обрамляет выделенный фрагмент текста каким-тегом нашего shortcode выглядит так:

ed.addCommand('mce-blogspot', function() {
    var newcontent = '[userid type="blogspot"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
    tinyMCE.activeEditor.selection.setContent(newcontent);
});
ed.addButton('blogspot', {
    title : 'blogspot',
    сmd : 'mce-blogspot',
    image : url + '/img/blogspot.gif'
});


Разумеется, пользователь, уже привыкший к тому, что наш плагин сам себя настраивает, захочет попытаться сгенерировать кнопки в цикле:
(function() {
	tinymce.create('tinymce.plugins.RikkiSocialIconsPlugin', {
	var newButtons = ["ljuser", "ljcomm", "liruman", "lirugirl", "ljr", "vk", "twitter"];
	tinymce.create('tinymce.plugins.LjusersPlugin', {
		init : function(ed, url) {
			var newButtonsLength = newButtons.length, i = 0;
			while(i < newButtonsLength){
				var itemTitle = newButtons[i];
				var itemCommand = 'mce'+itemTitle;
				ed.addCommand(itemCommand, function() {
					var newcontent = '[userid type="'+itemTitle+'"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
					tinyMCE.activeEditor.selection.setContent(newcontent);
				});
				ed.addButton(itemTitle, {
					title : itemTitle,
					cmd : itemCommand,
					image : url + '/img/'+itemTitle+'.gif'
				});
				i++;
			}
		},
		getInfo : function() {
			return {
				longname : 'Rikki\'s WP Social Icons',
				author : 'Rikki Mongoose',
				authorurl : 'http://rikkimongoose.ru',
				infourl : 'http://rikkimongoose.ru/projects/rikkis-wp-social-icons/',
				version : "1.0"
			};
		}
	});
	tinymce.PluginManager.add('rikkisocialicons', tinymce.plugins.RikkiSocialIconsPlugin);
})();


И то верно — разве не должны за программиста работать роботы?

Если сгенерировать кнопки таким образом, а потом перейти на editor, то сразу почувствуешь гордость за своё мастерство. Кнопки, указанные в array-е, выстроились в ряд, и на каждой — та самая иконка, которую увидит посетитель сайта или читатель RSS-ленты. Очень удобно!

Этот код выглядит замечательно, но у него есть один-единственный недостаток — он не работает. Это первый баг, который подстерегает вас в tinyMCE. На первый взгляд кнопочки выглядят самыми обыкновенными — но если пощёлкать по ним, то оказывается, что каждая из них вставляет shortcode с последним элементом — в нашем случае с twitter.

Придётся писать всё руками через замыкания, как подсказывают в комментах. Примерно вот так:

(function() {
	var newButtons = ["ljuser", "google_plus", "wordpress", "habrahabr", "github"];
	tinymce.create('tinymce.plugins.RikkiSocialIconsPlugin', {
	init : function(ed, url) {
		for ( i in newButtons) {
			var itemTitle = newButtons[i];
			(function(itemTitle) {
				var itemCommand = 'mce'+itemTitle;
				ed.addCommand(itemCommand, function() {
					var newcontent = '[userid type="'+itemTitle+'"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
					tinyMCE.activeEditor.selection.setContent(newcontent);
				});
				ed.addButton(itemTitle, {
					title : itemTitle,
					cmd : itemCommand,
					image : url + '/img/'+itemTitle+'.gif'
				});
			})(itemTitle);
		}
	},
		getInfo : function() {
			return {
				longname : 'Rikki\'s WP Social Icons',
				author : 'Rikki Mongoose',
				authorurl : 'http://rikkimongoose.ru',
				infourl : 'http://rikkimongoose.ru/projects/rikkis-wp-social-icons/',
				version : "1.0"
			};
		}
	});
	tinymce.PluginManager.add('rikkisocialicons', tinymce.plugins.RikkiSocialIconsPlugin);
})();


Тестирование показало, что этот вариант отлично работает. Слава комментаторам!

Этот вариант уже намного лучше — каждая кнопка знает своё место и срабатывает, как надо.

Наконец, есть ещё второй баг, который зависит исключительно от вас. И, хотя Wordpress, tinyMCE и даже Internet Explorer тут виноваты разве что в не очень удобной обработке ошибки, он может попортить вам немало крови.

Возможная ошибка касается функции tinymce.PluginManager.add(param1, param2). Пожалуйста, пишите её очень внимательно.

param1 должен совпадать с ключом, в который мы добавляли массив id новых кнопок в нашем php-файле. В моём случае там должен быть 'rikkisocialicons' (т.к. в PHP у нас $plugin_array['rikkisocialicons']).
param2 должен совпадать с тем, что создаётся через tinymce.create(). В моём случае это tinymce.plugins.RikkiSocialIconsPlugin, (т.к. в скрипте у нас написано tinymce.create('tinymce.plugins.RikkiSocialIconsPlugin')

Если что-то из этого не будет совпадать — у вашего editor-а пропадёт панель с кнопками, а консоль сообщит про ошибку 'k is undefined', которая произошла… разумеется, в сжатой jQuery.min.js, так что ни отладить, ни посмотреть вызов не будет ни малейшей возможности.

Как добавить иконку для сервиса X?


Сначала неплохо сходить на GitHub и посмотреть — вдруг уже появился fork с нужной иконкой? Если нет — тогда спасаемся своими руками.
  1. Сохраняем её в формате gif в ту же директорию, где лежат остальные иконки
  2. Добавляем в $options ещё один параметр, где ключ совпадает с именем gif-а, а URL — с url-ом, который нужно подставлять, причём имя пользователя заменяем на %s (пользуясь случаем, передаю привет всем C++ программистам, которые на этом месте наверняка испытают ностальгию).
  3. Если хотите кнопку идёте в JavaScript — добавляете элемент с тем же id в массив newButtons
  4. Упаковываете всё в ZIP
  5. Отключаете и удаляете старую версию плагина. Не беспокойтесь, shortcode в ваших постах при этом не пострадают
  6. Загружаете обновлённую версию, активируете её и наслаждаетесь


Очерёдность кнопок зависит от очерёдности в PHP-файле.

Вот и всё!



Плагин лежит в каталоге Wordpress. Там же, или на GitHub-е, вы можете порадовать автора, сообщив, что теперь и ваш stand-alone блог украшен модными иконками. А ещё можете дополнить проект иконками dreamwidth-а или ещё какой-нибудь xanga.

Добавляем в каталог



Остался последний шаг — добавление плагина в каталог Wordpress, чтобы его можно было найти стандартным поиском.

Разумеется, нужно не забыть добавить предварительно readme.txt и стандартные комментарии в заголовке плагина. Иначе пользователь не сможет узнать, что же он ставит.

Вся процедура подробно описана в большом англоязычном мануале.

А для тех, кому не терпится — вот короткое пошаговое руководство:

  1. Идём на wordpress.org и создаём там учётную запись
  2. Получаем пароль. Заходим под ним и идём на страницу добавления plugin-а. Закидываем туда ссылку на ZIP, пишем название и описание, и отправляем Post.
  3. Теперь нужно подождать. Плагин из мануала рассматривали 18 часов, с моим уложились в часа 4. Когда рассмотрение закончилось и всё получилось, на сервере появится пустая SVN-директория, в которую надо будет залить ваш проект
  4. Создаём локально папку, делаем туда checkout и копируем наш plug-in в trunc. Сжимать ZIP-ом не надо — после commit-а в trunc скрипт на сервере сожмёт всё автоматически.
  5. Делаем commit.
  6. Идём на страницу http://wordpress.org/extend/plugins/rikkis-wp-social-icons/. Вместо rikkis-wp-social-icons подставьте название вашего плагина.
  7. Идём на страницу http://wordpress.org/extend/plugins/ и смотрим внимательно на список Newest Plugins. Вот он, наш красавец!


Если что-то не заработало — обратитесь к большому мануалу. Там всё очень подробно расписано.

См. также


  1. В каталоге Wordpress
  2. Официальное представительство на GitHub
  3. Скачать c GitHub
  4. Оригинальная статья от Jenyay — часть 1, часть 2, часть 3.


Автор будет благодарен всем, кто возьмёт шефство над github-овской версией проекта и будет развивать его дальше.
Теги:phpjavascriptpluginwordpresswordpress plugin
Хабы: WordPress PHP JavaScript
+15
25,4k 122
Комментарии 50
Лучшие публикации за сутки