Свой плагин, установленный на десяток сайтов, обновляют обычно так: собрать архив, зайти в админку каждого сайта, загрузить, заменить. Через полгода на сайтах стоят пять разных версий, и никто не помнит, где какая. Каталог 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.

· полезно?
АЧ
Алексей Чикун веб-разработчик, команда TRIOHEAD

Делаю сайты и сервисы целиком: от вёрстки до сервера. Пишу о том, что проверил на своих проектах.

Комментарии

// загружаю комментарии…

Комментарии публикуются после проверки. Ссылки и HTML не принимаются.