Як тема перекриває twig-шаблон модуля
Модуль везе свою вітринну розмітку у власному пакеті:
app/Modules/<Studly>/Views/<назва>.twig. Правити цей файл — очевидний і
неправильний хід: це копія пакета, тож наступний реліз модуля або нічний
cms9:auto-update або перезапише правку, або взагалі відмовиться оновлювати
модуль і повідомить local_changes (див.
Локальні правки в модулі).
Тема — єдине місце на сайті, якого не чіпає ні пакет модуля, ні збірка ядра. Тож розмітка, яку сайт хоче змінити, належить саме туди:
themes/<тема>/modules/<identif>/<шлях усередині Views>.twig
Якщо цей файл є, вітрина рендерить його замість вью модуля. Більше не
змінюється нічого: модуль оновлюється як звичайно, а cms9:module-drift
лишається чистим, бо всередині app/Modules/ ніхто нічого не чіпав.
<тема>— активна тема (settings.site_template);<identif>— кодовий ідентифікатор модуля (share,buy_one_click,payment_method_2checkout), а не назва теки в StudlyCase;<шлях усередині Views>— шлях точно такий, як підViews/модуля, разом із підтеками.
Приклад: модуль share рендерить app/Modules/Share/Views/share.twig.
Тема base перекриває його файлом:
themes/base/modules/share/share.twig
Повна заміна вью
Скопіюйте вью модуля за шляхом вище і правте копію:
mkdir -p themes/base/modules/share
cp app/Modules/Share/Views/share.twig themes/base/modules/share/share.twig
Перекриття підхоплюється на наступному запиті — реєструвати його ніде не треба і кеш чистити теж (див. Кеш).
Змінити один блок замість копії файла: @module
Повна копія заморожує розмітку модуля на день копіювання: кожна пізніша правка в модулі на цьому сайті невидима, а команда дрейфу може лише сказати, що оригінал поїхав, — злити зміни за вас вона не вміє. Коли треба змінити лише фрагмент, розширюйте оригінал, а не копіюйте його.
Оригінальне вью завжди доступне під twig-простором імен @module:
{% extends '@module/share/share.twig' %}
{% block share_label %}
<span class="my-label">Поділитися сторінкою</span>
{% endblock %}
@module/<identif>/<шлях>.twig завжди веде у файл самого модуля і ніколи
назад у тему — тож файл може безпечно розширювати саме те вью, яке перекриває:
рекурсії немає за побудовою.
{% include %} працює так само — це варіант на випадок, коли вью модуля не
оголошує жодного {% block %} (більшість наразі не оголошує):
<div class="my-wrapper">
{% include '@module/share/share.twig' %}
</div>
Блоки — кращий контракт: якщо у вью модуля ядра потрібен блок, додайте його в сам модуль, а не копіюйте файл до себе.
На що перекриття має право вказувати
Перекриття замкнене в теці активної теми. Резолвер приймає лише шлях, який
лишається під themes/<тема>/modules/, і відмовляє всьому іншому: сегментам
.., абсолютним шляхам, порожнім і здвоєним роздільникам, symlink-у з ціллю
поза темою, NUL-байту в назві та будь-якому розширенню, крім .twig.
Відмова — не помилка: рендериться вью самого модуля, наче перекриття й не
було.
Що не переживає зміну теми
Перекриття належать одній темі. Зміните settings.site_template — і всі
перекриття попередньої теми перестають діяти, повертаються вью модулів.
Копія теми (або передача її іншому сайту) несе перекриття з собою, бо це
звичайні файли всередині теки теми.
Теми не входять у core-zip, тож перекриття невидиме і для cms9:core-drift,
і для збірки ядра. Тримайте його в репозиторії самої теми, як і будь-який
інший її файл.
Виняток — unishop. Цю тему закріплює гейт HTML-парності CrawlSweep, тож
перекриття для неї свідомо вимкнені: файли під themes/unishop/modules/
команда дрейфу перелічує, але рендер їх не бере. Те саме обмеження вже діє
для BaseWidget::themeOverride.
Коли перекриття зламане
Синтаксична помилка Twig або виняток усередині перекриття не гасить сторінку.
Рендерер відкочується на вью модуля і пише один рядок у writable/logs/:
ERROR - 2026-09-24 05:57:38 --> theme module override share/share.twig failed: Unclosed "(" in "@__sfovr/share/share.twig" at line 2.
Рівень саме error, а не warning, і це навмисно: у проді
Config\Logger::$threshold = 4, тож warning до лога не дійшов би — сторінка
мовчки рендерила б не те, що задумав сайт.
Дрейф: cms9:theme-overrides
Перекриття — це фотографія вью модуля в один момент. Модуль оновлюється, і в оригіналі може з'явитись поле, клас чи цілий блок, про які перекриття не знає, — при цьому нічого голосно не ламається, сайт просто тихо малює торішню розмітку.
php spark cms9:theme-overrides # активна тема
php spark cms9:theme-overrides base # названа тема
php spark cms9:theme-overrides base --module share # один модуль
php spark cms9:theme-overrides base --json # машинний вивід
php spark cms9:theme-overrides base --write-hash # підписати поточний стан
Обидві форми опції працюють — --module share і --module=share.
Команда лише читає, окрім --write-hash. Вона звіряє маркер sha256 у
першому рядку перекриття з поточним вью модуля:
{# cms9-override-of: sha256=782c403f0f71ef880492815406c151becb7a95bdf1b0880f9f46b7fee15bb4c8 #}
Маркер лежить у самому файлі, а не в окремому індексі, бо файли перекриттів копіюють, перейменовують і комітять поодинці — зовнішній індекс від цього розходиться мовчки. Twig-коментар не потрапляє у вивід, а перекриття без маркера так само рендериться.
Стани:
| Стан | Що означає | Код виходу |
|---|---|---|
ok | маркер збігається з поточним вью модуля | 0 |
unpinned | маркера немає — дрейф для цього файла не відстежується | 0 |
drift | вью модуля змінилось після підпису перекриття | 1 |
orphan | модуля або перекритого вью вже немає | 1 |
Тож CI може просто запустити команду: 0 — усі підписані перекриття ще
збігаються, 1 — хтось має подивитись. Помилка вжитку дає 7
(EXIT_USER_INPUT) і з дрейфом не плутається.
Порядок дій після оновлення модуля:
php spark cms9:theme-overrides base # drift → які вью поїхали
php spark cms9:module-drift share --diff # що саме змінилось в оригіналі
# перенести зміну в перекриття, далі:
php spark cms9:theme-overrides base --module share --write-hash
Кеш
Руками чистити нічого не треба. Кеш скомпільованого twig ключується шляхом
знайденого файла, а перекриття рендериться під іншою назвою шаблона, ніж вью
модуля, — тож поява, зникнення чи правка перекриття дають інший
скомпільований шаблон уже на наступному запиті. Правку на місці покриває
auto_reload, який звіряє filemtime.
Кешується натомість готовий HTML: з увімкненим LiteSpeed-кешем гостьова
сторінка віддає попередню розмітку, доки кеш не почистять. Чистьте штатно —
консоллю кеша в адмінці (Optimizer), яка шле X-LiteSpeed-Purge: *. Файли
кеша руками не видаляйте. Щоб перевірити зміну, нічого не чистячи, запитайте
сторінку з унікальним query-рядком (?nc=1) — він обходить гостьовий кеш.
Адмінка
Перекриття діють лише на вітрині. Шаблони адмінки йдуть іншим рендерером і під це не підпадають.