Какова ваша лучшая практика для выполнения одноразовых скриптов?
Проблема
Мы все сталкивались с подобной ситуацией, и многие вопросы на этом сайте требуют подобного решения. Вам нужно либо обновить базу данных, автоматически вставить много данных, конвертировать meta_keys
или что-то подобное.
Конечно, в работающей системе, основанной на лучших практиках, такого происходить не должно.
Но поскольку это случается, я хотел бы услышать ваше личное решение этой проблемы и почему вы выбрали именно его.
Вопрос
Как вы реализуете одноразовые скрипты в вашей (работающей) установке WordPress?
Проблема здесь в основном из-за следующих причин:
- Скрипты, которые вставляют данные, не должны выполняться более одного раза
- Скрипты, требующие много ресурсов, не должны выполняться в то время, когда за ними нельзя наблюдать
- Они не должны запускаться случайно
Причина, по которой я спрашиваю
У меня есть своя практика, я опубликую ее в ответах. Поскольку я не знаю, является ли это лучшим решением из существующих, я хотел бы узнать о вашем. Кроме того, этот вопрос часто задается в контексте других вопросов, и было бы здорово иметь ресурс, собирающий все идеи.
с нетерпением жду, чтобы научиться у вас :)

Лично я использую комбинацию следующих подходов:
- отдельный файл для одноразового скрипта
- использование транзиента (transient) для предотвращения случайного повторного запуска скрипта
- управление правами пользователей (capability-management или user-control) для гарантии, что скрипт запускается только мной
Структура
Я использую файл (onetime.php
) в папке inc
, который подключается в functions.php
и удаляется оттуда после использования.
include( 'inc/onetime.php' );
Файл для самого скрипта
В моем onetime.php
размещена функция f711_my_onetime_function()
. Это может быть любая функция. Предполагается, что ваш скрипт уже протестирован и работает корректно.
Для контроля выполнения скрипта я использую:
Контроль прав доступа
Чтобы предотвратить случайный запуск скрипта другими пользователями:
if ( current_user_can( 'manage_options' ) ) // проверка прав администратора
или
if ( get_current_user_id() == 711 ) // проверка, что это я - я предпочитаю ограничивать выполнение только для себя, а не для всех админов.
Транзиент
чтобы предотвратить случайный повторный запуск скрипта мной же.
$transient = 'f711_my_onetime_check';
if ( !get_transient( $transient ) ) // проверка, что функция еще не выполнялась.
Файл для выполнения скрипта в моей функции f711_my_onetime_function()
будет выглядеть так:
$transient = 'f711_my_onetime_check';
if ( get_current_user_id() == 711 && !get_transient( $transient ) ) {
set_transient( $transient, 'locked', 600 ); // блокировка функции на 10 минут
add_action( 'wp_footer', 'f711_my_onetime_function' ); // выполнение моей функции на нужном хуке.
}
function f711_my_onetime_function() {
// вся моя великолепная одноразовая магия.
}
Я устанавливаю транзиент сразу после проверки его существования, потому что хочу, чтобы функция выполнялась после того, как скрипт будет защищен от повторного запуска.
Если мне нужен вывод из функции, я либо вывожу его в виде комментария в подвале, либо иногда фильтрую контент.
Время блокировки установлено на 10 минут, но может быть изменено по вашему усмотрению.
Очистка
После успешного выполнения скрипта я удаляю include
из functions.php
и файл onetime.php
с сервера. Так как я использовал временный транзиент, мне не нужно чистить базу данных, но вы также можете удалить транзиент после удаления файла.

Вы также можете сделать следующее:
запустите onetime.php
и переименуйте его после выполнения.
if ( current_user_can( 'manage_options' ) ) {
if( ! file_exists( '/path/to/onetime.php' ) )
return;
add_action( 'wp_footer', 'ravs_my_onetime_function' ); // выполнить мою функцию на нужном хуке.
}
function ravs_my_onetime_function() {
// вся моя великолепная одноразовая магия.
include( '/path/to/onetime.php' );
// после выполнения переименуйте файл;
rename( '/path/to/onetime.php', '/path/to/onetime-backup.php');
}

Я создал командный скрипт Phing для этого, в нём нет ничего особенного, кроме загрузки внешнего скрипта для выполнения. Причина, по которой я использовал его через CLI:
- Я не хочу, чтобы он загружался случайно (нужно ввести команду)
- Это безопасно, так как может выполняться вне корневой веб-директории, то есть он может влиять на WP, но WP не может получить доступ к скрипту никаким образом.
- Он не добавляет никакого кода в WP или саму базу данных.
require('..путь к ../wp-blog-header.php');
//куча глобальных переменных WP
define('WP_USE_THEMES', false);
//пользовательский код
Таким образом, вы можете использовать Phing или PHP CLI и спать спокойно. WP-CLI также является хорошей альтернативой, хотя я не помню, можно ли использовать его вне корневой веб-директории.
Поскольку это популярный пост, вот пример скрипта: https://github.com/wycks/WordPhing (run.php)

В идеальных условиях я бы подключился к серверу через SSH и выполнил функцию самостоятельно, используя wp-cli.
Однако это не всегда возможно, поэтому я обычно устанавливаю переменную $_GET и подключаю её к хуку 'init', например:
add_action( 'init', function() {
if( isset( $_GET['one_time'] ) && $_GET['one_time'] == 'an_unlikely_string' ) {
do_the_one_time_thing();
}
});
затем перехожу по ссылке
http://my_blog.com/?one_time=an_unlikely_string
и отключаю хук после выполнения.

Ещё один довольно простой способ запуска одноразового скрипта — использование MU-плагина.
Поместите код в PHP-файл (например, one-time.php
), загрузите его в папку MU-плагинов (по умолчанию /wp-content/mu-plugins
), настройте права доступа к файлу, запустите плагин (т.е., в зависимости от выбранного хука, вам достаточно просто посетить фронтенд/бэкенд сайта), и всё готово.
Вот шаблон для такого скрипта:
/**
* Главный (и единственный) класс.
*/
class OneTimeScript {
/**
* Хук для функции плагина.
*
* @type string
*/
public static $hook = 'init';
/**
* Приоритет функции плагина.
*
* @type int
*/
public static $priority = 0;
/**
* Запуск одноразового скрипта.
*
* @hook self::$hook
* @return void
*/
public static function run() {
// здесь выполняем одноразовое действие...
// очистка
add_action('shutdown', array(__CLASS__, 'unlink'), PHP_INT_MAX);
} // function run
/**
* Удаление файла.
*
* @hook shutdown
* @return void
*/
public static function unlink() {
unlink(__FILE__);
} // function unlink
} // class OneTimeScript
add_action(OneTimeScript::$hook, array('OneTimeScript', 'run'), OneTimeScript::$priority);
Без комментариев и дополнительных элементов код выглядит так:
class OneTimeScript {
public static $hook = 'init';
public static $priority = 0;
public static function run() {
// здесь выполняем одноразовое действие...
add_action('shutdown', array(__CLASS__, 'unlink'), PHP_INT_MAX);
} // function run
public static function unlink() {
unlink(__FILE__);
} // function unlink
} // class OneTimeScript
add_action(OneTimeScript::$hook, array('OneTimeScript', 'run'), OneTimeScript::$priority);

Конечно, вы можете создать одноразовый код в виде плагина.
add_action('admin_init', 'one_time_call');
function one_time_call()
{
/* ВАШИ СКРИПТЫ */
deactivate_plugins('onetime/index.php'); //деактивировать текущий плагин
}
Вопрос: как активировать этот плагин без нажатия на ссылку "Активировать"?
Просто добавьте activate_plugins('onetime/index.php');
в functions.php
Или используйте must-use плагины: http://codex.wordpress.org/Must_Use_Plugins
Попробуйте разные действия для выполнения одноразового плагина:
admin_init - после инициализации админки
init - инициализация WordPress
wp - когда WordPress загружен

Иногда я использовал функцию, подключенную к деактивации плагина.
Смотрите здесь Обновление старых ссылок на красивые постоянные ссылки для пользовательских типов записей
Поскольку только администраторы могут активировать плагины, проверка прав доступа является побочным эффектом.
Нет необходимости удалять файл при деактивации — WordPress не будет его включать. Кроме того, если вы захотите запустить его снова, вы можете это сделать. Просто активируйте и деактивируйте его снова.
А иногда я использовал временные данные (transient), как в ответе @fischi. Например, здесь запрос для создания товаров WooCommerce из изображений или здесь Удаление/замена тегов img в контенте записей для автоматически публикуемых постов
Комбинация обоих подходов может быть альтернативой.

Это тоже очень хорошая идея. Если станет раздражать постоянная необходимость активировать и деактивировать плагин, можно также привязать эту же функцию к активации плагина, верно?

Да, если хотите. Однако я считаю, что два клика — это не такая уж большая нагрузка для выполнения одноразового скрипта. Любое другое решение, включающее команды CLI или работу с файлами (переименование, удаление), требует больше "усилий". Кроме того, каждый раз, когда вы полагаетесь на хуки, вы зависите от глобальных переменных, добавляя дополнительный слой потенциальных проблем с безопасностью/предсказуемостью кода. @fischi

Другой способ — установить глобальную wp_option, когда работа завершена, и проверять эту опцию каждый раз при выполнении хука init.
function my_one_time_function() {
// Выходим, если работа уже выполнена.
if ( get_option( 'my_one_time_function', '0' ) == '1' ) {
return;
}
/***** ВЫПОЛНИТЕ ВАШУ ОДНОРАЗОВУЮ РАБОТУ *****/
// Добавляем или обновляем wp_option
update_option( 'my_one_time_function', '1' );
}
add_action( 'init', 'my_one_time_function' );
Естественно, вам не нужно хранить этот код вечно (даже если это просто чтение из базы данных), поэтому вы можете удалить его после завершения работы. Также вы можете вручную изменить значение этой опции на 0, если вам нужно повторно выполнить код.

Использование wp-cli eval-file
— это отличный способ. Вы даже можете выполнять его на удаленной системе (используя ssh-алиас через '@') с локальным скриптом.
Если ваш код находится в файле one-time.php
в корневой директории WordPress, и у вас есть доступ к командной строке на системе, против которой нужно выполнить команду, вы можете сделать так:
wp eval-file one-time.php
Если файл one-time.php
находится локально, и вы хотите выполнить его на удаленном WordPress, используя @, команда будет выглядеть так:
wp @remote eval-file - < one-time.php

Мой подход немного отличается. Я предпочитаю добавлять одноразовые скрипты как функцию в файл function.php моей темы и запускать их по определенному GET-запросу.
if ( isset($_GET['linkupdate']) ) {
add_action('init', 'link_update', 10);
}
function link_update() {
// Одноразовый скрипт
die;
}
Чтобы запустить этот скрипт, просто перейдите по URL "www.sitename.com/?linkupdate"
Пока что этот метод отлично работает для меня...
Есть ли у этого способа какие-либо недостатки? Просто интересно...

Я просто использую один пользовательский шаблон страницы товара, который не задействован и не подключен ни к чему на публичном сервере.
Например, если у меня есть страница с отзывами, которая не опубликована (в черновике или другом статусе), но привязана к шаблону отдельной страницы, например single-testimonial.php
— я могу разместить функции внутри этого файла, загрузить страницу через preview
, и функция (или что-то другое) будет запущена один раз. Также очень легко вносить изменения в функцию при отладке.
Это действительно просто, и я предпочитаю такой подход использованию хука init
, потому что у меня больше контроля над тем, когда и как функция запускается. Просто мои личные предпочтения.

На всякий случай, если это поможет, вот что я сделал, и это хорошо работает:
add_action( 'init', 'upsubscriptions_setup');
function upsubscriptions_setup()
{
$version = get_option('upsubscriptions_setup_version');
// Если версия еще не записана в БД
if (!$version) {
add_option('upsubscriptions_setup_version', '0.1');
$version = get_option('upsubscriptions_setup_version');
}
if (version_compare($version, "0.1") <= 0) {
// выполнить действия
update_option('upsubscriptions_setup_version', '0.2');
}
if (version_compare($version, "0.2") <= 0) {
// выполнить действия
update_option('upsubscriptions_setup_version', '0.3');
}
if (version_compare($version, "0.3") <= 0) {
// выполнить действия
update_option('upsubscriptions_setup_version', '0.4');
}
// и т.д.
}
