WP-CLI завантажує звичайний wp-load.php у CLI-процесі PHP. Ніякого HTTP, ніякого max_execution_time від PHP-FPM, свій memory_limit і при цьому повний доступ до ядра, активних плагінів та всіх хуків. Команда диспетчеризується вже після того, як завантажились mu-plugins, плагіни й тема, тому WooCommerce чи Yoast докидають власні підкоманди, а ваш код може докинути свої. Глобальні прапорці --skip-plugins і --skip-themes дають дістатися до бази навіть тоді, коли фатал у плагіні не пускає в адмінку.
Найчастіше WP-CLI ставлять заради перенесення сайту між доменами, навіть на проєктах без деплой-пайплайна. WordPress зберігає масиви й об'єкти через serialize(), а той пише довжину кожного рядка: s:24:"https://shop.example.com". Заміна домену через sed по дампу або через SQL REPLACE() міняє вміст і не чіпає число, після чого unserialize() повертає false, і налаштування теми, віджети, кастомайзер та метадані білдерів зникають без жодної помилки в логах. wp search-replace розбирає такі значення рекурсивно, підміняє рядок і серіалізує назад. За замовчуванням він ходить тільки по таблицях із $wpdb->tables, тож для таблиць плагінів потрібен --all-tables-with-prefix або --all-tables. --skip-columns=guid обов'язковий: guid для фідів працює як постійний ідентифікатор запису, а не як адреса сторінки. --dry-run спершу покаже, скільки й де зміниться. Прапорець --precise вимикає оптимізацію через SQL REPLACE() і жене кожен рядок через PHP: повільніше, зате коректно там, де в одному полі змішані серіалізовані й звичайні дані.
WP-Cron взагалі не cron. Під час завантаження сторінки WordPress перевіряє чергу і, якщо щось прострочене, робить неблокуючий loopback-запит на wp-cron.php. Звідси й наслідки: сайт без відвідувачів не виконує нічого, за повносторінковим кешем події спрацьовують зрідка, а на staging із базовою HTTP-авторизацією loopback не проходить взагалі. Робоча схема: define('DISABLE_WP_CRON', true) у wp-config.php плюс рядок у crontab, який кожні кілька хвилин викликає wp cron event run --due-now. Поставити crontab і не вимкнути внутрішній механізм гірше, ніж не ставити зовсім: події почнуть запускатись паралельно з двох джерел. Для розбору wp cron test перевіряє прохідність loopback, wp cron event list показує прострочені події з next_run_relative, а wp cron event run <hook> запускає конкретний хук вручну.
Власні команди перетворюють WP-CLI на місце, куди складають усю рутину: перерахунок залишків, реіндексацію, розсилку, разові міграції даних. WP_CLI::add_command('shop resync-stock', [new Shop_CLI(), 'resync']) реєструє метод, а синопсис, валідацію аргументів і вивід wp help WP-CLI будує з докблока за блоком ## OPTIONS. Довгі цикли ганяйте партіями через WP_CLI\Utils\make_progress_bar() і скидайте wp_cache_flush() між партіями, інакше внутрішній кеш об'єктів з'їдає пам'ять на десятках тисяч записів. Для автоматизації найбільше важить те, чим команда завершується: WP_CLI::error() пише в STDERR і виходить із кодом 1, а echo плюс return дає код 0, і пайплайн із set -e вважає провалену міграцію успішною.
Обмеження теж називайте вголос. Транзакційності WP-CLI не дає: якщо search-replace впаде посередині на базі в кілька гігабайт, ви отримаєте наполовину замінені дані, тому перед ним завжди йде wp db export, а сама заміна на живому проді робиться у вікні обслуговування. wp db export і wp db import викликають зовнішні mysqldump і mysql, яких у мінімалістичному контейнері може не бути. Запуск від root під --allow-root залишає файли в wp-content/uploads із власником root, після чого веб-сервер не може їх перезаписати. Файлова частина міграції теж лишається за кадром: wp-config.php, конфіг nginx і захардкоджені URL у темі жоден search-replace не полагодить.
#!/usr/bin/env bash
# Оновлення staging дампом з продакшену. Запускати з кореня WordPress.
set -euo pipefail
OLD='https://shop.example.com'
NEW='https://staging.shop.example.com'
# Ті самі URL, як їх зберігають page builders усередині JSON
OLD_JSON='https:\/\/shop.example.com'
NEW_JSON='https:\/\/staging.shop.example.com'
wp maintenance-mode activate
wp db export "backup/staging-$(date +%F-%H%M).sql" --porcelain
wp db import dump/prod-latest.sql
# Холостий прогін показує кількість замін по кожній таблиці й колонці
wp search-replace "$OLD" "$NEW" --all-tables --skip-columns=guid --dry-run
# Реальна заміна: значення з serialize() розпаковуються і збираються назад
wp search-replace "$OLD" "$NEW" --all-tables --skip-columns=guid --report-changed-only
wp search-replace "$OLD_JSON" "$NEW_JSON" --all-tables --report-changed-only
wp core update-db # схема бази з prod буває старішою за код staging
wp cache flush # Redis досі віддає старі опції з домену prod
wp rewrite flush --hard
# Staging не має індексуватись і слати листи покупцям
wp option update blog_public 0
wp plugin deactivate woocommerce-payments mailchimp-for-wp
# WP-Cron вимкнено в wp-config.php, події тягне системний crontab
wp cron event list --fields=hook,next_run_relative,recurrence
wp cron event run --due-now --quiet
wp maintenance-mode deactivate
Скажіть, що будь-який search-replace починаєте з --dry-run і --skip-columns=guid, а після імпорту бази робите wp cache flush. На проєктах із Redis половина «містичних» багів одразу після переносу пояснюється саме тим, що об'єктний кеш ще віддає старі опції.