Свой плагин, установленный на десяток сайтов, обновляют обычно так: собрать архив, зайти в админку каждого сайта, загрузить, заменить. Через полгода на сайтах стоят пять разных версий, и никто не помнит, где какая. Каталог wordpress.org не подходит: плагин ваш, внутренний или просто не для публикации. Но WordPress с версии 5.8 умеет брать обновления плагина с любого сервера — и кнопка «Обновить» в админке работает как для плагинов из каталога.
Это вторая часть цикла «Мастерская разработчика». Здесь — клиентская часть, то, что живёт внутри плагина. Сервер и выпуск релизов из CI — в третьей части.
Update URI: «не спрашивай wordpress.org»
В заголовок главного файла плагина добавляется строка:
/**
* Plugin Name: ACH Thumbnail
* Version: 1.3.1
* Update URI: https://alekseychikun.ru/plugins/ach-thumbnail/
*/
Она решает две задачи. Первая — защита: без неё WordPress сверяет плагин с каталогом по имени папки, и если в каталоге окажется чужой плагин с тем же слагом, ваш сайт предложит «обновиться» на него. С Update URI ядро в каталог за этим плагином не ходит. Вторая — точка подключения: вместо каталога ядро вызывает фильтр update_plugins_{домен}, где домен берётся из Update URI. Здесь это update_plugins_alekseychikun.ru.
Фильтр проверки обновления
Фильтр получает данные плагина и должен вернуть массив с версией и адресом архива. Сравнивать версии не нужно — ядро сделает это само и покажет обновление, только если версия на сервере новее:
add_filter('update_plugins_alekseychikun.ru', function ($update, $pluginData, $pluginFile) {
if ($pluginFile !== $this->basename) {
return $update; // фильтр общий для всех плагинов с этого домена
}
$info = $this->fetch(); // справка с сервера, см. ниже
if (!$info) {
return $update;
}
return [
'slug' => $this->slug,
'version' => $info['version'],
'url' => $info['homepage'] ?? '',
'package' => $info['download_link'],
'requires' => $info['requires'] ?? '',
'tested' => $info['tested'] ?? '',
'requires_php' => $info['requires_php'] ?? '',
'icons' => $info['icons'] ?? [],
];
}, 10, 3);
Дальше всё штатное: обновление появляется в списке плагинов и на странице «Обновления», ставится кнопкой, работают и автообновления.
Окно «Подробнее»
Ссылка «Посмотреть детали версии» открывает окно с описанием и списком изменений. Его данные приходят через фильтр plugins_api. Если сервер отдаёт справку в формате api.wordpress.org/plugins/info — поля name, version, sections, banners и т. д. — достаточно вернуть её объектом:
add_filter('plugins_api', function ($result, $action, $args) {
if ($action !== 'plugin_information' || ($args->slug ?? '') !== $this->slug) {
return $result;
}
$info = $this->fetch();
return $info ? (object) $info : $result;
}, 20, 3);
Вкладки окна берутся из ключей sections. Для известных ключей (description, installation, changelog) WordPress подставляет переведённые заголовки, а для неизвестных показывает сам ключ. Поэтому раздел «Использование» на сервере называется прямо по-русски — 'Использование', а не usage.
Кэш: админка не должна ждать сервер
WordPress проверяет обновления дважды в сутки и при заходах на некоторые страницы админки. Без своего кэша каждая такая проверка и каждое открытие окна «Подробнее» — запрос к вашему серверу, а при недоступном сервере — до 10 секунд ожидания.
private function fetch(): ?array
{
$cached = get_site_transient($this->cacheKey);
if (is_array($cached)) {
return $cached['ok'] ? $cached['data'] : null;
}
$res = wp_remote_get(self::API . rawurlencode($this->slug) . '?installed=' . rawurlencode($this->version), ['timeout' => 10]);
$data = null;
if (!is_wp_error($res) && (int) wp_remote_retrieve_response_code($res) === 200) {
$json = json_decode(wp_remote_retrieve_body($res), true);
if (!empty($json['version']) && !empty($json['download_link'])) {
$data = $json;
}
}
// успех помним 12 часов, неудачу — час: недоступный сервер не тормозит каждую страницу
set_site_transient($this->cacheKey, ['ok' => $data !== null, 'data' => $data], $data ? 12 * HOUR_IN_SECONDS : HOUR_IN_SECONDS);
return $data;
}
И две точки сброса кэша: после установки обновления (upgrader_process_complete) и по кнопке «Проверить снова» на странице обновлений — иначе она показывала бы кэш, а не ответ сервера.
Один клиент на много плагинов
Клиент — один файл с классом, который копируется в каждый плагин без изменений. Если на сайте стоят два плагина с разными редакциями этого файла, загрузится только первый класс, и второй плагин будет работать чужим кодом. Поэтому номер редакции — в имени класса:
if (!class_exists('ACH_Plugin_Updater_1')) {
final class ACH_Plugin_Updater_1 { /* … */ }
}
// в главном файле плагина
require_once __DIR__ . '/includes/class-ach-plugin-updater.php';
new ACH_Plugin_Updater_1(__FILE__, 'ach-thumbnail', '1.3.1');
Несовместимая правка клиента — новый класс ACH_Plugin_Updater_2; старые плагины продолжают работать с первым.
Первый переход — руками. Плагин, в котором клиента ещё нет, сам о сервере не узнает. Версию с клиентом на каждый сайт надо поставить один раз вручную — дальше обновления придут сами.
Свои плагины на десятке сайтов обновляются руками?
Напишите — подключу обновления из админки с вашего сервера, без каталога wordpress.org.
// загружаю комментарии…