Мультисайтовость в 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/sharedmv /home/www/site1/bitrix /home/www/shared/bitrixmv /home/www/site1/upload /home/www/shared/uploadmv /home/www/site1/local /home/www/shared/local
ln -s /home/www/shared/bitrix /home/www/site1/bitrixln -s /home/www/shared/upload /home/www/site1/uploadln -s /home/www/shared/local /home/www/site1/local
ln -s /home/www/shared/bitrix /home/www/site2/bitrixln -s /home/www/shared/upload /home/www/site2/uploadln -s /home/www/shared/local /home/www/site2/localРезультат в корне второго сайта:
$ ls -la /home/www/site2/lrwxrwxrwx bitrix -> /home/www/shared/bitrixlrwxrwxrwx upload -> /home/www/shared/uploadlrwxrwxrwx 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 в фильтре. Правится добавлением второго сайта в привязку инфоблока.
Можно ли держать сайты одной установки на разных редакциях или версиях ядра?
Нет. Все сайты одной установки работают на одном ядре, одной базе данных и одной редакции продукта - раздельных администраторов или разных редакций для разных сайтов лицензия не допускает. Число сайтов ограничивает сама редакция: например Старт позволяет создать не более двух сайтов, остальные редакции такого ограничения не имеют. Независимые сайты с отдельной копией ядра и отдельной базой на разных серверах - это уже не мультисайтовость, а разные установки со своими лицензионными условиями.
Связанные темы
- Архитектура платформы - концепция мультисайтовости, жизненный цикл запроса и константы SITE_ID/LANGUAGE_ID, на которые опирается практика в этой статье.
- Инфоблоки - структура типов, разделов и свойств, поверх которой строится привязка контента к сайту.
- Интернет-магазин на 1С-Битрикс - Basket::create() и Order::create() и остальной D7 API продаж с обязательным SITE_ID.
- Шаблоны сайта - как назначают и переключают шаблон дизайна, включая условия показа по сайтам.
- Композитный сайт - альтернатива HTML-кешу для мультисайтовости на разных доменах.
- BitrixVM и веб-окружение - пул сайтов окружения
- nginx и PHP-FPM - серверные блоки под каждый домен
- Слетает авторизация: время жизни сессии и запоминание входа - общий вход на сайтах пула
- Перенос сайта на другой сервер - переезд сайтов пула
- Сайты в пуле BitrixVM: добавление, каталоги, права и доступ - несколько сайтов на одном сервере
- Несколько баз и организаций: разведение заказов - заказы двух сайтов в одной учётной базе
- Поддомены и второй сайт: одна установка или две - выбор схемы на практике
- Раздел Инфраструктура
- Код для двух сайтов: текущий сайт, настройки, общие данные - практика для второго сайта