Перейти к содержимому

Мультисайтовость в 1С-Битрикс - несколько сайтов на одной копии платформы

Общий принцип мультисайтовости - несколько сайтов на одном ядре и одной базе. Он разобран в статье «Архитектура платформы». Там же объяснено, что сайт хранится записью в таблице с доменом, папкой и языком. Идентификатор текущего сайта доступен в константе SITE_ID. Здесь собрана практика. Как физически завести второй сайт и что настроить на веб-сервере. Где идентификатор сайта передают в код руками, а где платформа подставит его сама. И что чаще всего ломается, когда второй сайт добавляют в уже работающий проект.

Как это работает

Два способа развести сайты различаются по сложности, а не только по внешнему виду. На одном домене сайты различаются только папкой. У каждого сайта в настройках заполняется «Папка сайта», например /s1/ и /s2/. Поле «Доменное имя» остаётся пустым, а «Путь к корневой папке веб-сервера» не используется вовсе. Отдельная настройка веб-сервера не нужна - Apache или nginx продолжают обслуживать один и тот же DOCUMENT_ROOT, а платформа сама решает по URL, какому сайту отдать страницу. На разных доменах устройство физически другое: у каждого сайта «Папка сайта» = /, заполнены домен и путь к корню веб-сервера, а сам сайт живёт в собственном DOCUMENT_ROOT. Общими остаются только /bitrix/, /upload/ и /local/ - и не копией, а символическими ссылками на единственный физический экземпляр: копировать ядро на второй сайт нельзя, это противоречит условиям лицензии на продукт. Каждому дополнительному сайту такой конфигурации нужны отдельный виртуальный хост и файл .access.php в его корне - без него первый же посетитель вместо страницы получит требование HTTP-авторизации.

Текущий сайт определяется по двум полям, а не по одному. Сопоставление идёт на раннем этапе пролога, до init.php и до открытия сессии. Платформа сверяет запрос с записями сайтов по «Доменному имени» и по «Папке сайта». У домена сравнивается правая часть после точки, поэтому поддомены попадают под свой основной домен. «Папка сайта» - это путь в URL, а не в файловой системе. Если по домену подходит несколько сайтов - например, у поддомена и у основного домена запись совпадает, - решает сортировка: побеждает сайт с меньшим значением, поэтому сортировка сайта на поддомене всегда должна быть меньше сортировки сайта на основном домене. Официальная документация не описывает явно, что происходит, если ни один сайт не подошёл вовсе; у каждого сайта есть служебный признак DEF («Сайт по умолчанию», Y/N) - по опыту сообщества именно на него в такой ситуации опирается платформа. Принудительно определить SITE_ID до пролога тоже можно - это отдельно разобрано в статье об архитектуре - тогда автоопределение по домену и папке отключается целиком.

За пределами обычного запроса сайта нет вовсе. В административном разделе SITE_ID соответствует языку интерфейса, а не сайту, поэтому сайтовый файл /local/php_interface/<ID сайта>/init.php там не подключается - только общий. В агентах на cron ситуация ещё жёстче: там нет ни SITE_ID, ни $USER, и код агента не может опираться на контекст конкретного сайта - его нужно либо перебирать сайты явно, либо не привязывать логику к сайту вовсе. Это ровно та причина, по которой код, годами работавший на одном сайте с захардкоженной константой или её отсутствием, начинает вести себя странно в момент, когда сайтов становится два.

Не всё разделяется по сайтам одинаково. Пользователи, группы и права - общие для всей установки просто потому, что это одна база: зарегистрировавшись на одном сайте, посетитель авторизован и на остальных, в пределах своих разрешений. Раздельными администраторами или разными редакциями для разных сайтов лицензия не наделяет - весь набор сайтов работает на одной редакции. Контент, корзина, шаблоны почтовых событий, типы плательщиков и шаблоны дизайна, наоборот, разделяются. Каждый такой объект привязывают к конкретному сайту явно. У него есть своё поле или своя связь с сайтом. Без неё объект просто не появится там, где его ждут. Дальше в примерах видно, где эту привязку платформа берёт на себя, а где её нужно проставлять руками.

Примеры

1. Второй сайт в подпапке одного домена

Создание сайта на одном домене - работа только в административном разделе, без единой строчки в конфигурации веб-сервера. У нового сайта заполняют двухсимвольный идентификатор (s2), название, шаблон и язык по умолчанию, и главное - «Папку сайта»: путь от корня, где физически лежит публичная часть именно этого сайта.

/home/www/site/ - общий DOCUMENT_ROOT
├── bitrix/ - общее ядро
├── upload/ - общие файлы
├── local/ - общий пользовательский код
├── s1/ - публичная часть первого сайта, "Папка сайта" = /s1/
│ └── index.php
└── s2/ - публичная часть второго сайта, "Папка сайта" = /s2/
└── index.php

Поля «Доменное имя» и «Путь к корневой папке веб-сервера» у обоих сайтов остаются пустыми. После сохранения запрос example.ru/s2/catalog/ система относит ко второму сайту: по домену оба совпадают (он либо не задан, либо общий), поэтому решает папка в пути. Оборотная сторона удобства - сайты физически соседствуют в одном дереве, а /local/ и /bitrix/ общие в буквальном смысле, а не только по конфигурации: правка в /local/ мгновенно затрагивает оба сайта.

2. Второй сайт на отдельном домене: симлинки и виртуальный хост

На разных доменах ядро не копируют, а один раз выносят в общее место и подключают к корню каждого сайта символическими ссылками:

Окно терминала
mkdir -p /home/www/shared
mv /home/www/site1/bitrix /home/www/shared/bitrix
mv /home/www/site1/upload /home/www/shared/upload
mv /home/www/site1/local /home/www/shared/local
ln -s /home/www/shared/bitrix /home/www/site1/bitrix
ln -s /home/www/shared/upload /home/www/site1/upload
ln -s /home/www/shared/local /home/www/site1/local
ln -s /home/www/shared/bitrix /home/www/site2/bitrix
ln -s /home/www/shared/upload /home/www/site2/upload
ln -s /home/www/shared/local /home/www/site2/local

Результат в корне второго сайта:

$ ls -la /home/www/site2/
lrwxrwxrwx bitrix -> /home/www/shared/bitrix
lrwxrwxrwx upload -> /home/www/shared/upload
lrwxrwxrwx local -> /home/www/shared/local
-rw-r--r-- .htaccess (скопирован с первого сайта)
-rw-r--r-- .access.php (скопирован с первого сайта, обязателен)
-rw-r--r-- 404.php (скопирован с первого сайта)
-rw-r--r-- index.php

Файл .access.php копируют отдельно и осознанно - без него второй сайт требует авторизацию у каждого посетителя:

<? $PERM["/"]["*"]="R"; ?>

Дополнительному сайту такой конфигурации нужен и собственный виртуальный хост:

<VirtualHost *:80>
ServerAdmin admin@site2.ru
DocumentRoot "/home/www/site2"
ServerName site2.ru
ServerAlias *.site2.ru
ErrorLog logs/site2-error.log
CustomLog logs/site2-access.log common
</VirtualHost>

В настройках этого сайта в административном разделе «Папка сайта» = /, заполнены «Доменное имя» (site2.ru) и «Путь к корневой папке веб-сервера» (/home/www/site2) - второе поле не используется при разводке по подпапкам, но обязательно здесь: по нему платформа находит физическую папку сайта при резервном копировании и восстановлении.

3. Шаблон и меню для каждого сайта

Шаблон назначается не самому файлу шаблона, а сайту. В «Настройки → Настройки продукта → Сайты» у сайта указывают сразу несколько шаблонов. У каждого своё условие показа и свой индекс сортировки. Условие - это PHP-выражение вроде CSite::InDir(), которое должно вернуть true. Выбор конкретного шаблона происходит на отдельном шаге пролога, ещё до вывода шапки. Для двух сайтов из первого примера результат настройки может выглядеть так:

СайтПапка сайтаШаблонУсловие показа
s1/s1/mainвсегда
s2/s2/main_enвсегда

Файлы меню .menu.php физически лежат в каждом каталоге рабочей области. При разводке сайтов по подпапкам они и так не пересекаются. Файлы s1/catalog/.menu.php и s2/catalog/.menu.php независимы без дополнительных условий. Общий для обоих сайтов пункт меню, наоборот, помечают условием CSite::InDir() или CSite::InGroup() в пятом элементе массива пункта. Отдельно от файлов часть настроек модуля «Управление структурой» - типы меню и их параметры - тоже задаётся по сайтам через административный интерфейс, а не только через расположение файлов.

4. Выборка элементов инфоблока текущего сайта

Сам инфоблок привязан к сайту (или сразу к нескольким) через связь Bitrix\Iblock\IblockSiteTable - в легаси-коде это поле SITE_ID/LID при создании инфоблока (CIBlock::Add()). У элемента отдельного поля «сайт» физически нет. Но CIBlockElement::GetList() всё равно принимает SITE_ID прямо в фильтре, как и синонимы LID, IBLOCK_SITE_ID, IBLOCK_LID. Связь через привязку инфоблока платформа выполняет сама:

$res = \CIBlockElement::GetList(
['SORT' => 'ASC'],
['IBLOCK_ID' => $iblockId, 'SITE_ID' => SITE_ID, 'ACTIVE' => 'Y'],
false,
false,
['ID', 'NAME', 'DETAIL_PAGE_URL']
);
while ($el = $res->GetNext()) {
$items[] = $el;
}

Без фильтра SITE_ID выборка вернёт элементы инфоблока независимо от того, каким сайтам он назначен. На сайте s2 в списке неожиданно окажутся позиции, рассчитанные на s1. На практике встречаются два сценария. Один инфоблок привязывают сразу к нескольким сайтам, когда контент одинаковый: например, общий справочник брендов. Либо заводят по инфоблоку на каждый сайт того же типа, когда контент различается. Тогда IBLOCK_ID для запроса тоже берут не константой, а находят по текущему сайту:

$res = \CIBlock::GetList([], ['TYPE' => 'catalog', 'SITE_ID' => SITE_ID, 'ACTIVE' => 'Y']);
$arIBlock = $res->Fetch();

5. Почтовое событие с правильным SITE_ID

Шаблон почтового события привязан к конкретному сайту и языку - при создании шаблона в административном разделе поле «Сайт» обязательно, а язык шаблона должен совпадать с языком этого сайта. При генерации письма платформа берёт язык интерфейса и кодировку из настроек сайта, к которому привязан использованный шаблон. Ни текущий запрос, ни настройки получателя на это не влияют. Событие, отправленное без указания сайта или не с тем сайтом, уйдёт клиенту на чужом языке или вовсе с другим шаблоном.

use Bitrix\Main\Mail\Event;
Event::send([
'EVENT_NAME' => 'NEW_ORDER',
'LID' => SITE_ID,
'C_FIELDS' => [
'EMAIL' => $email,
'ORDER_ID' => $order->getId(),
],
]);

LID здесь - строка, а не массив, даже если у самого события несколько привязанных сайтов. В легаси-коде тот же параметр - второй позиционный аргумент, CEvent::Send($event, $lid, $fields). Код, который раньше писался для одного сайта, часто не получает LID явно. Тогда заказ со второго сайта уходит с шаблоном первого. Причина одна: где-то в цепочке вызовов идентификатор сайта захардкожен ещё с тех времён, когда сайт был один.

Справочник

Определение и контекст сайта

ЭлементТипНазначениеОсобенности
SITE_IDконстантаидентификатор текущего сайта в публичной частиопределяется в прологе по домену и папке; заданная до пролога константа отключает автоопределение
Context::getCurrent()->getSite()D7тот же идентификатор в новом стиледоступен через Bitrix\Main\Application
LANGUAGE_ID, SITE_CHARSET, SITE_TEMPLATE_ID, SITE_DIRконстантыязык, кодировка, шаблон и папка текущего сайтаопределяются на том же шаге пролога, что и SITE_ID
Поле «Доменное имя» сайтанастройкапервый критерий определения сайтасравнивается правая часть после точки, поддомены учитываются автоматически
Поле «Папка сайта»настройкавторой критерий, путь в URLрешает, если несколько сайтов совпали по домену
Сортировка сайтанастройкаразрешает конфликт при полном совпадении критериевпобеждает меньшее значение
DEFполе CSite / SiteTableпризнак «Сайт по умолчанию» (Y/N)точный момент использования документация не раскрывает
bitrix:main.site.selectorкомпонентпереключатель между сайтами мультисайтовой конфигурациивывод меняется копированием шаблона компонента

Где идентификатор сайта нужен явно

МестоЯвная привязкаОсобенности
ИнфоблокSITE_ID/LID в CIBlock::Add(), связь IblockSiteTableодин инфоблок можно привязать сразу к нескольким сайтам
Элементы инфоблокафильтр SITE_ID в CIBlockElement::GetList()своего поля «сайт» у элемента нет, платформа джойнит через привязку инфоблока
Корзина\Bitrix\Sale\Basket::create(SITE_ID), поле LID записикорзины разных сайтов не пересекаются автоматически
Заказ\Bitrix\Sale\Order::create(SITE_ID, $userId)обязательный первый параметр
Типы плательщиковполе LID в CSalePersonType::Add()значение может быть массивом сайтов
Расчёт цены со скидкамипараметр $siteID в CCatalogProduct::GetOptimalPrice()правила скидок и купонов зависят от сайта
Почтовое событиеLID в Event::send(), второй параметр CEvent::Send()шаблон события сам привязан к сайту и языку
Шаблон дизайнаназначение в «Настройки → Сайты», а не в самом шаблонеу сайта может быть несколько шаблонов с условиями показа
Сайтовый init.phpпуть /local/php_interface/<ID сайта>/init.phpне подключается в административном разделе

Что не требует явной привязки

ЧтоПочему
Пользователи, группы, праваодна база данных на всю установку, привязки к сайту нет
Автоопределение SITE_ID в публичной частиделает платформа на этапе пролога
/bitrix/, /upload/, /local/общие папки при любом способе мультисайтовости

Частые ошибки

На втором сайте вместо страницы - запрос HTTP-авторизации. Симптом: на разных доменах сайт не открывается, браузер требует логин и пароль у любого посетителя. Причина - в корне дополнительного сайта нет файла .access.php; при разводке через симлинки его копируют с первого сайта вручную, сам он не создаётся.

Обновление одного сайта на разных доменах затрагивает другой неожиданным образом. Симптом - правки, тестировавшиеся на одном сайте, тут же проявляются на другом. Причина обычно не в баге, а в устройстве: /bitrix/, /upload/ и /local/ при мультисайтовости на разных доменах общие через симлинки, и это ожидаемое поведение - копировать ядро вместо этого нельзя, такая конфигурация не соответствовала бы лицензии.

Домены в настройках сайтов указали с www или оставили неактуальный алиас. Симптом - куки и всплывающие окна срабатывают «с чужого сайта», часть контента одного сайта показывается в шаблоне другого. Причина - домены нужно указывать без www, только реальные и актуальные; лишняя или неверная запись путает определение сайта и замедляет систему.

HTML-кеш перестал работать после перехода на мультисайтовость с разными доменами. Симптом - страницы, которые раньше кешировались, снова считаются заново на каждый запрос. Причина - классическое HTML-кеширование не рассчитано на разные домены без дополнительной настройки; вместо него на разных доменах используют композитный сайт.

Письмо ушло не на том языке или не по тому шаблону. Симптом - клиент второго сайта получает уведомление на языке первого сайта. Причина - SITE_ID/LID не передан явно в вызов почтового события; язык и кодировка письма берутся из настроек сайта, к которому привязан использованный шаблон, а не из контекста текущего запроса.

Код вне обычного хита путает сайт или не видит его вовсе. Симптом - AJAX- обработчик, консольный скрипт или агент, который раньше работал через глобальную константу SITE_ID, на многосайтовом проекте берёт значение не того сайта либо получает пустую строку. Причина - вне публичной части обычного запроса (агенты, часть обработчиков, отдельные скрипты) сайт нужно определять и передавать явно - константу нельзя один раз подключить и забыть о ней.

Частые вопросы

Чем принципиально отличаются два способа мультисайтовости и как выбрать между ними?

Сайты в подпапках одного домена проще: не нужна отдельная настройка веб-сервера, оба сайта физически живут в одном DOCUMENT_ROOT, различие только в поле Папка сайта. Такой способ обычно выбирают для языковых или региональных версий одного проекта. Сайты на разных доменах устроены иначе: у каждого свой DOCUMENT_ROOT, а общими остаются только /bitrix/, /upload/ и /local/, подключённые символическими ссылками, плюс отдельная настройка виртуального хоста и файл .access.php в корне каждого дополнительного сайта. Его выбирают, когда сайты тематически не связаны и должны выглядеть независимыми проектами.

Что произойдёт, если два сайта совпадут по домену?

Решает поле Папка сайта: система сравнит путь запроса и выберет сайт, чья папка совпала. Если и это совпадение не однозначно, например домен настроен и у поддомена, и у основного сайта, решает сортировка - выигрывает сайт с меньшим значением, поэтому у сайта на поддомене сортировка всегда должна быть меньше, чем у сайта на основном домене.

Общие ли пользователи, авторизация и корзина между сайтами одной установки?

Пользователи, группы и права общие: это одна база данных на всю установку, отдельного набора учётных записей на сайт не бывает. С авторизацией сложнее на разных доменах: браузер не передаёт куки одного домена другому, поэтому без включённой опции Распространять авторизацию на все сайты вход придётся проходить на каждом домене заново. Корзина разделена по сайтам физически - в записи корзины есть поле LID, а Basket::create() требует идентификатор сайта первым параметром, так что содержимое корзин разных сайтов не смешивается само по себе.

Почему только что созданный инфоблок не показывается на новом сайте?

Инфоблок привязывается к сайтам отдельно от своего содержимого - через связь IblockSiteTable, в легаси-коде это поле SITE_ID или LID инфоблока. Если при создании инфоблока отметили только первый сайт, второй его просто не увидит - ни в компонентах с фильтром по текущему сайту, ни в выборках с SITE_ID в фильтре. Правится добавлением второго сайта в привязку инфоблока.

Можно ли держать сайты одной установки на разных редакциях или версиях ядра?

Нет. Все сайты одной установки работают на одном ядре, одной базе данных и одной редакции продукта - раздельных администраторов или разных редакций для разных сайтов лицензия не допускает. Число сайтов ограничивает сама редакция: например Старт позволяет создать не более двух сайтов, остальные редакции такого ограничения не имеют. Независимые сайты с отдельной копией ядра и отдельной базой на разных серверах - это уже не мультисайтовость, а разные установки со своими лицензионными условиями.

Связанные темы

Первоисточники