<? phpukraine СТАТТІ
⌕Пошук по платформі
WORDPRESS 30 вересня 2026 · 7 хв читання

WordPress на Composer: Bedrock, плагіни як залежності і деплой

WordPress звик сам собі перезаписувати файли, і саме через це деплой з git регулярно відкатує плагін, який хтось оновив з адмінки минулого тижня. Bedrock прибирає цю двозначність: ядро й плагіни стають залежностями Composer, конфіг переїжджає у змінні середовища, а продакшн залишається read-only. Розбираємо структуру, WPackagist, .env і порядок команд при релізі.

РP
Редакція phpukraine
Редакція платформи

Сайт лежить, у логах фатал у плагіні кешування, останній деплой був годину тому і містив правку одного шаблону. Причина зазвичай не в правці. Хтось із доступом до адмінки натиснув «Оновити» два тижні тому, оновлена версія плагіна опинилась тільки на продакшн-диску, а деплой привів дерево файлів до того стану, який лежить у git. Формально це відкат. На ділі стара версія плагіна опинилась над базою, яку вже перебудувала нова.

Чому оновлення з адмінки ламають деплой

WordPress проєктувався так, щоб сайт міг сам себе обслуговувати: WP_Filesystem пише в wp-content/plugins, розпаковує zip, підміняє каталоги. Для сайту без розробника це зручно й геть несумісне з будь-яким деплоєм, де файли беруться з репозиторію. Виходить два джерела істини: git і живий диск. Розходження між ними видно лише тоді, коли деплой уже перезаписав каталог.

Гірше, що оновлення плагіна майже ніколи не обмежується файлами. При активації або на хуку upgrader_process_complete плагін читає власну опцію версії (щось у стилі myplugin_db_version), виконує dbDelta(), додає колонку, конвертує серіалізовані дані, пише нове значення опції. База поїхала вперед. Коли деплой повертає код назад, опція лишається новою: міграцій «вниз» у WordPress не існує, і плагін або тихо не виконує потрібний апгрейд, або падає на колонці, якої в його версії ще немає.

Додайте туди ж ручні правки в темі через вбудований редактор, плагін, поставлений «на тест» і забутий, і різницю в наборі плагінів між стейджингом і продом. Нормальний вихід один: список файлів визначає composer.lock, а продакшн не має права його змінювати. Саме це й робить Bedrock, стандартна структура WordPress-проєкту від Roots, де ядро, плагіни й теми є пакетами Composer.

Структура Bedrock

Bedrock ставиться як шаблон проєкту: composer create-project roots/bedrock. Дерево виглядає так:

composer.json
composer.lock
.env                  # не в git
wp-cli.yml
config/
  application.php     # спільний конфіг
  environments/
    development.php
    staging.php
    production.php
web/                  # document root веб-сервера
  index.php
  wp-config.php       # вантажить корневий wp-config.php і wp-settings.php
  wp/                 # ядро, встановлює Composer, у git не лежить
  app/                # те, що раніше було wp-content
    plugins/
    mu-plugins/
    themes/
    uploads/          # не в git
vendor/
wp-config.php         # require config/application.php

Два зсуви тут принципові. Перший стосується document root: ним стає web/, а ядро лежить у підкаталозі web/wp. Тому WP_SITEURL дорівнює WP_HOME . '/wp', а wp-login.php живе за /wp/wp-login.php. Все, що поза web/, з інтернету недосяжне: .env, vendor, config.

Другий зсув: wp-content перейменований у web/app через WP_CONTENT_DIR і WP_CONTENT_URL. Ядро приходить пакетом roots/wordpress, який тягне roots/wordpress-no-content, те саме ядро, але без теки wp-content з дефолтними темами й Hello Dolly. Тека web/wp повністю керована Composer і додана в .gitignore, разом з web/app/uploads і каталогами плагінів, якими володіє Composer. Свою тему й свої mu-плагіни, звісно, тримаємо в git.

Розкладає пакети по правильних місцях composer/installers через секцію extra:

"extra": {
    "installer-paths": {
        "web/app/mu-plugins/{$name}/": ["type:wordpress-muplugin"],
        "web/app/plugins/{$name}/": ["type:wordpress-plugin"],
        "web/app/themes/{$name}/": ["type:wordpress-theme"]
    },
    "wordpress-install-dir": "web/wp"
}

Плагіни, які мусять завантажуватись першими (перемикачі мультисайту, фільтри на pre_option_*, аварійні kill switch), кладуть у mu-plugins. WordPress вантажить звідти лише файли першого рівня, тому Bedrock ставить roots/bedrock-autoloader, mu-плагін, який підхоплює і вкладені каталоги.

Плагіни з WPackagist

WPackagist дзеркалить каталог wordpress.org у форматі Composer-репозиторію. Підключається одним блоком:

"repositories": [
    {
        "type": "composer",
        "url": "https://wpackagist.org",
        "only": ["wpackagist-plugin/*", "wpackagist-theme/*"]
    }
]

only тут не косметика: без нього Composer питатиме WPackagist про кожен ваш пакет і роздує час резолву. Імена пакетів збігаються зі слагами з каталогу: composer require wpackagist-plugin/redirection поставить плагін зі слагом redirection у web/app/plugins/redirection.

Головна пастка тут у версіях. Плагіни wordpress.org живуть за власними правилами, і мінорний реліз там спокійно може змінити структуру таблиць або прибрати хук. Тому обмеження типу ^4.0 для плагіна ризикованіше, ніж для нормальної бібліотеки: беріть ~4.9.0 чи точну версію для всього, що критичне для бізнесу, і оновлюйте свідомо. Джерелом істини на сервері все одно є composer.lock, тому реальне оновлення виглядає як composer update wpackagist-plugin/redirection -W локально, прогін на стейджингу й коміт лок-файлу.

Якщо плагін має бути mu-плагіном, перевизначте шлях по імені пакета, а не по типу:

"web/app/mu-plugins/{$name}/": ["wpackagist-plugin/some-switch"]

Платні плагіни в WPackagist не потраплять. Варіанти: власний приватний Composer-репозиторій (наприклад, SatisPress, який віддає встановлені плагіни як пакети), або пакет, оголошений вручну з посиланням на zip вендора. Ліцензійний ключ у composer.json не зберігають: Composer підставляє змінні середовища прямо в URL:

{
    "type": "package",
    "package": {
        "name": "vendor/premium-plugin",
        "version": "3.1.2",
        "type": "wordpress-plugin",
        "dist": {
            "type": "zip",
            "url": "https://example.com/download?token={%PREMIUM_PLUGIN_KEY}"
        }
    }
}

Мінус такого пакета: версію ви оновлюєте руками, бо Composer не бачить списку релізів. Це нудно, зате явно видно в історії git.

Змінні середовища і конфіг

.env у корені читається через vlucas/phpdotenv, значення дістаються хелпером env() з oscarotero/env, а константи оголошуються через Roots\WPConfig\Config:

use Roots\WPConfig\Config;

Config::define('WP_HOME', env('WP_HOME'));
Config::define('WP_SITEURL', Config::get('WP_HOME') . '/wp');
Config::define('WP_DEBUG_LOG', env('WP_DEBUG_LOG') ?? false);
Config::define('DISALLOW_FILE_EDIT', true);

Config::apply();

Різниця з define() у тому, що Config тримає значення в масиві до apply() і кричить, якщо ту саму константу оголошено двічі з різними значеннями. Конфіг для оточення (config/environments/production.php) виконується після application.php і може перевизначати вже задане, бо apply() ще не викликаний; прибрати константу можна через Config::remove().

Мінімальний набір у .env: DB_NAME, DB_USER, DB_PASSWORD, DB_HOST, WP_ENV, WP_HOME, вісім солей (AUTH_KEY, SECURE_AUTH_KEY, LOGGED_IN_KEY, NONCE_KEY і чотири відповідні *_SALT). Солі генеруються один раз на оточення і не збігаються між стейджингом і продом, інакше сесійна cookie з одного середовища валідна в іншому.

Оскільки WP_HOME і WP_SITEURL приходять константами, вони перекривають опції home і siteurl у базі. Практичний наслідок: копію продакшн-бази можна розгорнути локально без search-replace по домену. Поля в адмінці стають нередагованими, і це правильно.

У продакшн-конфігу обов'язкові дві речі. DISALLOW_FILE_MODS (true) прибирає з адмінки встановлення й оновлення плагінів, тем і ядра разом з кнопками, тому дійти до розходження з git уже немає як. AUTOMATIC_UPDATER_DISABLED (true) вимикає фонові автооновлення. Задайте й WP_ENVIRONMENT_TYPE: цю константу ядра читає wp_get_environment_type(), і на неї спираються самі плагіни, тоді як WP_ENV лишається суто змінною Bedrock для вибору файла конфігу.

Деплой і оновлення

Bedrock не приносить свого деплоєра, він робить сайт придатним до звичайного build-and-release. Артефакт збирається не на проді:

composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
npm ci && npm run build

Далі каталог релізу кладеться поряд з попередніми, спільні шляхи підв'язуються симлінками (.env, web/app/uploads), і симлінк current перемикається одним ln -sfn. Після перемикання:

wp core update-db          # і --network для мультисайту
wp cache flush
wp rewrite flush

Про wp core update-db забувають найчастіше. Composer оновив файли ядра, але db_version в опціях лишився старим; доки апгрейдер не пройшов, WordPress або редіректить адмінів на /wp/wp-admin/upgrade.php, або працює на схемі, що не відповідає коду. Те саме стосується плагінів зі своїми міграціями: вони чекають запиту в адмінку, тому після релізу зайдіть явно на /wp/wp-admin/ (або викличте їхню CLI-команду, якщо є), щоб не дізнаватись про недороблену міграцію від першого відвідувача.

Свої нюанси в drop-in'ів. object-cache.php і advanced-cache.php лежать не в теці плагіна, а в корені web/app, і кладе їх туди плагін через запис на диск. У read-only релізі це не спрацює: файли треба або тримати в git, або копіювати кроком деплою після composer install. Так само перевірте плагіни, які пишуть у власний каталог (генератори sitemap, критичного CSS, локальних логів): їм потрібен шлях у uploads або окремий writable каталог зі спільних, інакше кожен реліз стирає їхній стан.

Оновлення тепер виглядає так само, як у будь-якому PHP-проєкті: composer update roots/wordpress для ядра, composer update wpackagist-plugin/... -W для плагіна, composer.lock у коміт, деплой на стейджинг з копією продакшн-бази, потім прод. Код відкатити легко, достатньо перемкнути симлінк назад. З базою так не вийде, тому реліз з міграціями завжди йде після бекапу, а не після сподівання.

ПИШЕТЕ ПРО PHP?Опублікуйте розбір або історію з проєкту на платформіРедактор із чеклістом, редактура, авторська сторінка. Републікація з блогу отримує canonical на оригінал. Відкрити редактор →
РP
Редакція phpukraine
Редакція платформи
Матеріали, які готує команда платформи на основі власних даних: каталогу вакансій, зарплатного звіту й банку питань. Кожна цифра в них рахується з бази, а не береться з голови.
оновлено 3 жовтня 2026 · ліцензія CC-BY-SA-4.0
ДАЛІ ПО ТЕМІ
ЧИТАТИ ДАЛІ
← Усі статті