[СДЕЛАЙ САМ] Контексты для регионов в интернет-магазине.
Задача: сделать максимально простое добавление новых контекстов на сайт для создания региональных копий с собственными ресурсами, robots.txt и sitemap.xml, уникальными для каждого региона ресурсами и ценами на некоторые товарные позиции.
Проблемы:
1. Вывод галереи изображений товаров
2. Добавление дополнительных категорий к товарам в новом контексте.
3. Связывание товаров.
Решение:
1. Настраиваем редирект со всех поддоменов на основной домен (как это сделать уточняйте у хостера или в Google);
2. Подключаем плагин для переключения контекстов:
Логика такая: определяем по url какой контекст запросили, если это не основной контекст (web), то переключаем контекст на запрошенный, в противном случае смотрим есть ли в $_COOKIE город, если нет, то устанавливаем $_COOKIE['curCity'], проверяем есть ли в списке контекстов запрошенный, если есть, то переключаем, если нет остаемся на основном контексте. На фронте этим управляет вот такой код
2.1 если кому интересно город я определяю через сайт DaData.Код сниппета detectRegion тут.
3. Подключаем плагин для создания настроек контекста, при копировании или создании нового.
Плагин рассчитан на работу с поддоменами. Но можно скорректировать и для работы с другим форматом url.
В процессе работы всплыл один неприятный нюанс, а именно: дополнительные категории у товаров не копировались, что неудивительно, но нужно было что-то делать. Поскольку при копировании контекста указывается только новый ключ и потом всё равно приходится заходить и прописывать его название, то я решил что модно использовать для запуска копирования дополнительных категорий ужа написанный плагин и использовал для этого сниппет setAddProductCategories
С 5 сотнями товаров и 3 сотнями записей в таблице modx_ms2_product_categories работает нормально.
Ещё очень важный момент — для всех шаблонов нужно создать TV linked_id куда для контекста web записать id текущего ресурса, для других контекстов id исходного ресурса. Например у вас есть контекст wtb с ресурсом Главная у которого id = 1. Если вы других ресурсов не создавали, то при копировании контекста с ресурсами в новом контексте назовем его new появится ресурс Главная c id = 2. Так вот в linked_id у ресурса с id =1 должно быть 1, и у ресурса с id=2 тоже 1. Такой подход даёт возможность не копировать галерею изображений товаров, а вызывать её из контекста web. Для этого нужен сниппет getLinkedResources, который на вход принимает строку из id разделенных запятой, а возвращает другую строку из связанных id разделенных запятой, если вернуться к примеру выше, то получив на вход 2 он вернёт 1.
Из кода видно что можно использовать как модификатор. Кейс применения такой. В контексте web есть ресурс у которого есть TV со списком id других ресурсов, в моём случае это были примеры работ, при копировании контекста эта TV копируется вместе со значениями, так вот чтобы не перезаполнять, я скармливают сниппету getLinkedResources то, что записано в этой TV, и получаю список id из нужного мне контекста. Пример вызова
И наверное последнее о чём стоит упомянуть это вывод списка городов, я для этого написал отдельный сниппет getAllContexts
Тут важно продублировать в настройках контекста web настройку site_url слеш в конце обязателен и прописать ctx_city. После этого можно выводить список городов вот так
Важно чтобы у всех связанных страниц был одинаковый псевдоним, это необходимо для того, чтобы при смене города в процессе просмотра сайта пользователь попадал не на главную страницу другого контекста, на такую же.
Кажется ничего не забыл. Спасибо за внимание. Всё.
Проблемы:
1. Вывод галереи изображений товаров
2. Добавление дополнительных категорий к товарам в новом контексте.
3. Связывание товаров.
Решение:
1. Настраиваем редирект со всех поддоменов на основной домен (как это сделать уточняйте у хостера или в Google);
2. Подключаем плагин для переключения контекстов:
<?php
// Работаем только на фронтенде
if ($modx->event->name != 'OnHandleRequest' || $modx->context->key == 'mgr') {return;}
// Определяем запрашиваемый хост
$host = $_SERVER['HTTP_HOST'];
$ctx = $modx->getObject('modContextSetting', array('key' => 'http_host', 'value' => $host));
if($ctx->get('context_key') != 'web'){
$modx->switchContext($ctx->get('context_key'));
}
Логика такая: определяем по url какой контекст запросили, если это не основной контекст (web), то переключаем контекст на запрошенный, в противном случае смотрим есть ли в $_COOKIE город, если нет, то устанавливаем $_COOKIE['curCity'], проверяем есть ли в списке контекстов запрошенный, если есть, то переключаем, если нет остаемся на основном контексте. На фронте этим управляет вот такой код
let btns = document.querySelectorAll('.jsChooseBtn'), // это кнопки подтверждения ДА и НЕТ
tooltip = document.querySelector('.jsCityTooltip'); // это само окно с вопросом "ЭТО ВАШ ГОРОД"
for(let i = 0; i < btns.length; i++){
btns[i].addEventListener('click', function(e){
tooltip.classList.add('d-none');
if(e.target.classList.contains('jsCityConfirm')){ // если нажали да
document.cookie = 'curCity='+ e.target.dataset.city + '; path=/;domain=ecodecking.ru';
}
});
}
if(document.cookie.indexOf('curCity') != -1){ // проверяем есть ли город в куках
tooltip.classList.add('d-none');
}
2.1 если кому интересно город я определяю через сайт DaData.Код сниппета detectRegion тут.
<?php
$url = 'https://suggestions.dadata.ru/suggestions/api/4_1/rs/iplocate/address';
$siteIsAvaliable = $modx->runSnippet('isSiteAvailible', array('url' => $url));
if($siteIsAvaliable){
$token = $modx->getOption('dadata_api_key');
$ip = $_SERVER['REMOTE_ADDR'];
$headers = array(
'Accept: application/json',
'Authorization: Token ' . $token
);
$ch = curl_init($url.'?ip='.$ip);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_HEADER, false);
$result = json_decode(curl_exec($ch),1);
curl_close($ch);
$region = $result['location']['data']['city'];
return $region;
}else{
$modx->log(1, 'detectRegion: Не возможно определить регион. Сервис DaData недоступен');
return false;
}
3. Подключаем плагин для создания настроек контекста, при копировании или создании нового.
<?php
switch ($modx->event->name) {
case 'OnContextSave':
$context_key = $context->get('key');
if($mode == 'new'){
$sql = 'SELECT AUTO_INCREMENT FROM information_schema.tables WHERE TABLE_NAME = "modx_site_content"';
$statement = $modx->query($sql);
$next_ids = $statement->fetchAll(PDO::FETCH_COLUMN);
$site_start = $next_ids[0];
$settings = array(
array(
'context_key' => $context_key,
'key' => 'base_url',
'value' => '/',
'xtype' => 'textfield',
'namespace' => 'core',
'area' => 'core',
'dictionary' => array(
'name' => 'setting_base_url',
'value' => 'Base URL',
'topic' => 'setting',
'namespace' => 'core',
'language' => 'ru',
'createdon' => date('Y-m-d H:i:s')
)
),
array(
'context_key' => $context_key,
'key' => 'error_page',
'value' =>$site_start,
'xtype' => 'textfield',
'namespace' => 'core',
'area' => 'core',
'dictionary' => array(
'name' => 'setting_error_page',
'value' => 'Page 404',
'topic' => 'setting',
'namespace' => 'core',
'language' => 'ru',
'createdon' => date('Y-m-d H:i:s')
)
),
array(
'context_key' => $context_key,
'key' => 'http_host',
'value' => $context_key .'.'. $modx->getOption('http_host'),
'xtype' => 'textfield',
'namespace' => 'core',
'area' => 'core',
'dictionary' => array(
'name' => 'setting_http_host',
'value' => 'Название домена',
'topic' => 'setting',
'namespace' => 'core',
'language' => 'ru',
'createdon' => date('Y-m-d H:i:s')
)
),
array(
'context_key' => $context_key,
'key' => 'site_start',
'value' => $site_start,
'xtype' => 'textfield',
'namespace' => 'core',
'area' => 'core',
'dictionary' => array(
'name' => 'setting_site_start',
'value' => 'Site start',
'topic' => 'setting',
'namespace' => 'core',
'language' => 'ru',
'createdon' => date('Y-m-d H:i:s')
)
),
array(
'context_key' => $context_key,
'key' => 'site_name',
'value' => $modx->getOption('site_name'),
'xtype' => 'textfield',
'namespace' => 'core',
'area' => 'core',
'dictionary' => array(
'name' => 'setting_site_name',
'value' => 'Site name',
'topic' => 'setting',
'namespace' => 'core',
'language' => 'ru',
'createdon' => date('Y-m-d H:i:s')
)
),
array(
'context_key' => $context_key,
'key' => 'site_url',
'value' => $modx->getOption('server_protocol').'://'.$context_key .'.'. $modx->getOption('http_host').'/',
'xtype' => 'textfield',
'namespace' => 'core',
'area' => 'core',
'dictionary' => array(
'name' => 'setting_site_url',
'value' => 'Site URL',
'topic' => 'setting',
'namespace' => 'core',
'language' => 'ru',
'createdon' => date('Y-m-d H:i:s')
)
),
array(
'context_key' => $context_key,
'key' => 'site_status',
'value' => 1,
'xtype' => 'combo-boolean',
'namespace' => 'core',
'area' => 'core',
'dictionary' => array(
'name' => 'setting_site_status',
'value' => 'Сайт опубликован',
'topic' => 'setting',
'namespace' => 'core',
'language' => 'ru',
'createdon' => date('Y-m-d H:i:s')
)
),
array(
'context_key' => $context_key,
'key' => 'ctx_city',
'value' => '',
'xtype' => 'textfield',
'namespace' => 'core',
'area' => 'core',
'dictionary' => array(
'name' => 'setting_ctx_city',
'value' => 'Город контекста',
'topic' => 'setting',
'namespace' => 'core',
'language' => 'ru',
'createdon' => date('Y-m-d H:i:s')
)
),
);
foreach ($settings as $k => $s){
if(!$modx->getObject('modContextSetting', array('key' => $s['key'], 'context_key' => $context_key))){
$new_setting = $modx->newObject('modContextSetting');
$new_setting->fromArray($s, '',true);
$new_setting->save();
}
if(!$modx->getObject('modLexiconEntry', array('name' => 'setting_'.$s['key']))){
$new_dictionary = $modx->newObject('modLexiconEntry');
$new_dictionary->fromArray($s['dictionary'],'',true);
$new_dictionary->save();
}
}
}
else{
$modx->runSnippet('setAddProductCategories', array('ctx' => $context_key));
}
break;
}
Плагин рассчитан на работу с поддоменами. Но можно скорректировать и для работы с другим форматом url.
В процессе работы всплыл один неприятный нюанс, а именно: дополнительные категории у товаров не копировались, что неудивительно, но нужно было что-то делать. Поскольку при копировании контекста указывается только новый ключ и потом всё равно приходится заходить и прописывать его название, то я решил что модно использовать для запуска копирования дополнительных категорий ужа написанный плагин и использовал для этого сниппет setAddProductCategories
<?php
if($ctx && $ctx != 'web' && $ctx != 'mgr'){
$resources = $modx->getIterator('modResource', array('context_key' => $ctx, 'class_key' => 'msProduct'));
foreach($resources as $r){
$sql = "SELECT category_id FROM modx_ms2_product_categories WHERE product_id = ".$r->getTVValue('linked_id');
$statement = $modx->query($sql);
$addCats = $statement->fetchAll(PDO::FETCH_COLUMN);
if($addCats){
foreach ($addCats as $cat) {
$addCat = $modx->getObject('modResource', $cat);
if($addCat){
$newAddCat = $modx->getObject('modResource', array(
'context_key' => $ctx,
'class_key' => 'msCategory',
'pagetitle' => $addCat->get('pagetitle')
));
if($newAddCat){
$s = array(
'product_id' => $r->get('id'),
'category_id' => $newAddCat->get('id')
);
$dbAddCat = $modx->getObject('msCategoryMember', $s);
if(!$dbAddCat){
$dbAddCat = $modx->newObject('msCategoryMember');
$dbAddCat->fromArray($s, '',true);
$dbAddCat->save();
}
}
}
}
}
}
}
С 5 сотнями товаров и 3 сотнями записей в таблице modx_ms2_product_categories работает нормально.
Ещё очень важный момент — для всех шаблонов нужно создать TV linked_id куда для контекста web записать id текущего ресурса, для других контекстов id исходного ресурса. Например у вас есть контекст wtb с ресурсом Главная у которого id = 1. Если вы других ресурсов не создавали, то при копировании контекста с ресурсами в новом контексте назовем его new появится ресурс Главная c id = 2. Так вот в linked_id у ресурса с id =1 должно быть 1, и у ресурса с id=2 тоже 1. Такой подход даёт возможность не копировать галерею изображений товаров, а вызывать её из контекста web. Для этого нужен сниппет getLinkedResources, который на вход принимает строку из id разделенных запятой, а возвращает другую строку из связанных id разделенных запятой, если вернуться к примеру выше, то получив на вход 2 он вернёт 1.
<?php
if($input){
$ctx = $options ?: 'web';
$output = array();
$sql = "SELECT contentid FROM modx_site_tmplvar_contentvalues WHERE tmplvarid = 176 AND value IN (".$input .")";
$statement = $modx->query($sql);
$ids = $statement->fetchAll(PDO::FETCH_COLUMN);
$sql = 'SELECT id FROM modx_site_content WHERE id IN ('.implode(',',$ids).') AND context_key = "'.$ctx.'"';
$statement = $modx->query($sql);
$output = $statement->fetchAll(PDO::FETCH_COLUMN);
return implode(',', $output);
}
Из кода видно что можно использовать как модификатор. Кейс применения такой. В контексте web есть ресурс у которого есть TV со списком id других ресурсов, в моём случае это были примеры работ, при копировании контекста эта TV копируется вместе со значениями, так вот чтобы не перезаполнять, я скармливают сниппету getLinkedResources то, что записано в этой TV, и получаю список id из нужного мне контекста. Пример вызова
{set $work_list = $_modx->resource.works_list | getLinkedResources : $_modx->context.key}
И наверное последнее о чём стоит упомянуть это вывод списка городов, я для этого написал отдельный сниппет getAllContexts
<?php
$sql = 'SELECT `key`, `value`, `context_key` FROM modx_context_setting';
$statement = $modx->query($sql);
$settings = $statement->fetchAll(PDO::FETCH_ASSOC);
$output = array();
foreach($settings as $set){
if($set['context_key'] != 'mgr'){
$output[$set['context_key']][$set['key']] = $set['value'];
}
}
return $output;
Тут важно продублировать в настройках контекста web настройку site_url слеш в конце обязателен и прописать ctx_city. После этого можно выводить список городов вот так
{set $settings = '!getAllContexts' | snippet}
{foreach $settings as $k => $set}
{if $k != $_modx->context.key}
{set $url = $set['site_url']}
{if $_modx->resource.alias != 'index'}
{set $alias = $_modx->resource.alias}
{/if}
<a href="{$url}{$alias}">{$set['ctx_city']}</a>
{/if}
{/foreach}
Важно чтобы у всех связанных страниц был одинаковый псевдоним, это необходимо для того, чтобы при смене города в процессе просмотра сайта пользователь попадал не на главную страницу другого контекста, на такую же.
Кажется ничего не забыл. Спасибо за внимание. Всё.
Поблагодарить автора
Отправить деньги