[СДЕЛАЙ САМ] Контексты для регионов в интернет-магазине.

Задача: сделать максимально простое добавление новых контекстов на сайт для создания региональных копий с собственными ресурсами, robots.txt и sitemap.xml, уникальными для каждого региона ресурсами и ценами на некоторые товарные позиции.

Проблемы:
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}

Важно чтобы у всех связанных страниц был одинаковый псевдоним, это необходимо для того, чтобы при смене города в процессе просмотра сайта пользователь попадал не на главную страницу другого контекста, на такую же.
Кажется ничего не забыл. Спасибо за внимание. Всё.
Артур
07 апреля 2021, 19:51
modx.pro
2
193
+3
Поблагодарить автора Отправить деньги

Комментарии: 0

Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
0