Посетитель

Ещё до того как заработает сессия и до первого запроса к базе, о пришедшем уже кое-что известно — из заголовков запроса. Откуда он подключился, с чего смотрит и на каком языке говорит. Эти три вещи отдают методы семейства Agent*.

Общее у них одно, и оно важное: данные приходят снаружи, от клиента. Ими определяют, что показать, но доверять им как проверенным сведениям нельзя — подделать заголовок может кто угодно.

Все три метода — для корневого скрипта. Внутри кешируемого модуля их вызывать нельзя: ответ «запечётся» в кеш и достанется посетителям, к которым он не относится. В модули эти сведения приходят уже в виде выбранной группы шаблонов, языка страницы или явно переданного параметра.

IP посетителя

$ip = MELBIS()->AgentIp();

Метод возвращает адрес посетителя с учётом того, что сайт может стоять за прокси или CDN, — разбираются заголовки пересылки, а не только прямой адрес соединения. Для журналов, антифрода и определения региона используйте именно его, а не $_SERVER['REMOTE_ADDR'] напрямую.

От сессии метод не зависит и работает независимо от того, вызван ли DefineSession.

Устройство

$device = MELBIS()->AgentDevice();

Метод возвращает одну из трёх строк — phone, tablet или desktop — и нужен там, где витрина отдаётся в разных версиях с одного адреса: по нему выбирается группа шаблонов до вызова Run().

Определение идёт в два шага. Сначала спрашивается сам браузер: Chromium по HTTPS присылает клиентскую подсказку Sec-CH-UA-Mobile, и это не догадка, а ответ браузера о себе. Если подсказки нет — Safari, Firefox и поисковые роботы её не шлют, — разбирается User-Agent по наборам ключей: сначала планшетные, затем телефонные. Не совпало ничего — desktop.

Планшет отделён от телефона намеренно: относить его к мобильной версии или к полной — решение проекта, а не движка.

$device = MELBIS()->AgentDevice();
if ( $device == 'phone' ) MELBIS()->TemplateSet('mobile');

MELBIS()->Run($entry_point, $entry_param);

Как устроены группы шаблонов и почему две версии витрины могут жить на одном домене — в разделе «Группы шаблонов».

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

MELBIS()->DefineAgentKeys([
    'tablet'    => '/ipad|tablet|playbook|silk|kindle|nexus 7/i'
    ]);

Ключ должен быть phone или tablet, значение — готовое регулярное выражение вместе с ограничителями и флагами. Неизвестный ключ или неверный шаблон останавливают парсер с ошибкой: иначе опечатка в регулярном выражении молча отправила бы весь мобильный трафик в полную версию. Подсказка Sec-CH-UA-Mobile не настраивается — портить хороший сигнал нечем.

Вердикт вычисляется один раз за запрос, поэтому звать метод повторно ничего не стоит.

Отдавая по одному адресу разную вёрстку, добавьте к ответу заголовок Vary: User-Agent — так поисковые системы и промежуточные кеши узнают, что содержимое страницы зависит от устройства.

Язык браузера

$lang = MELBIS()->AgentLanguage(['ru', 'uk'], 'uk');

Метод разбирает заголовок Accept-Language и возвращает язык, который посетитель предпочитает больше других, — двухбуквенным кодом. Первый аргумент — языки, которые умеет витрина; второй — что вернуть, если ни один из них посетителю не подходит. Без аргументов метод отдаёт самый желанный язык вообще, каким бы он ни был.

Нужен он там же, где AgentDevice, — в корневом скрипте, чтобы встретить нового посетителя на его языке: подставить нужный языковой префикс в адрес или сразу вызвать LanguageSet.

Почему нельзя просто взять первые две буквы заголовка. Браузер присылает не один язык, а список с весами:

uk-UA,uk;q=0.9,ru;q=0.8,en-US;q=0.7,en;q=0.6

Вес q (от 0 до 1, по умолчанию 1) и задаёт предпочтение — порядок элементов формально ничего не значит, хотя браузеры обычно пишут по убыванию. На заголовке en-US,ru;q=0.9,uk;q=0.8 наивное «первые два символа» дадут en, и посетитель, прямо попросивший русский вторым пунктом, получит язык по умолчанию. AgentLanguage выбрасывает из списка всё, чего витрина не умеет, и берёт максимальный вес среди оставшегося.

Ещё три вещи метод делает сам: приводит региональные варианты к языку (uk-UA и uk — это один uk, из них берётся больший вес), пропускает q=0 — это явный отказ от языка, а не слабое предпочтение, — и игнорирует * и мусор.

Заголовок разбирается один раз за запрос, поэтому повторные вызовы с разными списками языков ничего не стоят.

Заголовка может не быть вовсе — его не шлют поисковые роботы и часть API-клиентов. Тогда метод вернёт значение по умолчанию, и это нормальный путь, а не ошибка: робот должен получать основную языковую версию, а не результат угадывания.