Автосоздание ресурсов в контекстах-дублях

Задача:
Один сайт, несколько городов, один и тот же контент у всех, но разные данные (переменные).
Поначалу думал использовать Babel, но с ним не все так просто. Поэтому пришлось писать самому при поддержке неравнодушных Максима Кузнецова и Сергея Шлокова.

Решение:
  • Плагин для переключения контекстов (от Василия Наумкина)
  • Плагин на события: OnDocFormSave, OnDocFormDelete.
  • Снипет для вывода контекстов в качестве меню
  • Чанк вывода меню контекстов
  • Снипет для подтягивания полей ресурсов-оригиналов (чтоб не дублировать в базу весь контент к примеру)
  • Js-скрипт для переключения контекстов
Итак приступим…
1) Плагин для переключения контекстов отсюда bezumkin.ru/sections/tips_and_tricks/2439/ (Василию респект). Работаем на поддоменах. По настройке все брал со статьи Василия.
2) Плагин для работы с ресурсами в контекстах-дублях (создание, редактирование, удаление, восттановление):

<?php
if ($modx->event->name == 'OnDocFormSave') {
    // собираем дублированные контексты (web - оригинальный контекст)
	$contexts = $modx->getCollection('modContext', array('key:NOT IN' => ['mgr','web']));
	
	// получаем родителя создаваемого ресурса
	$parent = $resource->get('parent');
	if ($parent != '0') {
        $parentId = $modx->getObject('modResource', $parent);
        $parentAlias = $parentId->get('alias');
	}
    $alias = $resource->get('alias');
    $id = $resource->get('id');
    // проходимся по контекстам
	foreach ($contexts as $context) {
	    
	    $response = $modx->getObject('modResource', array('context_key'=>$context->key, 'alias' => $alias));
	    // если ресурс уже существует то тогда просто обновляем поля
	    if ($response) {
    	    $response->set('pagetitle', $resource->get('pagetitle'));
    	    $response->set('content', '[[!OriginalFields?&id=`'.$id.'`&field=`content`]]');
    	    $response->set('alias', $resource->get('alias'));
    	    $response->set('deleted', $resource->get('deleted'));
            //... еще много полей ресурса
    	    
    	    $response->setTVValue('title', $resource->getTvValue('title'));
            //... еще различные tv-шки

    	    $response->save();

        // если ресурса в контексте нет то создаем новый
	    } else {
	        // создание нового ресурса
    	    $newResource = $modx->newObject('modDocument');
    	    // заполняем поля ресурса
    	    $newResource->set('context_key', $context->key);
    	    $newResource->set('pagetitle', $resource->get('pagetitle'));
    	    $newResource->set('content', '[[!OriginalFields?&id=`'.$id.'`&field=`content`]]');
    	    $newResource->set('alias', $resource->get('alias'));
    	    $newResource->set('deleted', $resource->get('deleted'));
            //... еще много полей ресурса
    	    
    	    // поле родителя для создания дубля в нем а не в корне
    	    if ($parent != '0') {
    	        $res = $modx->getObject('modResource', array('context_key'=>$context->key, 'alias'=>$parentAlias));
    	        $parntId = $res->get('id');
    	    } else {
    	        $parntId = $parent;
    	    }
    	    $newResource->set('parent', $parntId);
    	    $newResource->save();
    	    
            // получаем id свежесозданного ресурса
    	    $docId = $newResource->get('id');
            // и заполняем различными tv-шками
            $tvs = $modx->getObject('modResource', $docId);
            //... еще различные tv-шки
            
            $tvs->save();
	    }
	}
	// очищаем кеш
	$modx->cacheManager->clearCache();
}

// удаление ресурсов в дублирующих контекстах
if ($modx->event->name == 'OnDocFormDelete') {
    // собираем дублированные контексты (web - оригинальный контекст)
    $contexts = $modx->getCollection('modContext', array('key:NOT IN' => ['mgr','web']));
    
    // проходимся по контекстам
    foreach ($contexts as $context) {
        
        // получаем нужные (верней не нужные поэтому и удаляемые) нам ресурсы
        $response = $modx->getObject('modResource', array('context_key'=>$context->key, 'alias' => $resource->get('alias')));
        
        // помечаем как удаленные
        $response->set('deleted', $resource->get('deleted'));
        $response->save();
        
        // удаляет полностью
        // $response->get('id');
        // $response->remove();
    }
	$modx->cacheManager->clearCache();
}
Вообще было бы идеально сделать заполнение/обновление полей ресурса через перебор вроде foreach, но пока руки остановились на этом.
Не забываем, плагин должен реагировать на события OnDocFormSave, OnDocFormDelete.

3) Снипет вывода контекстов как меню
<?php
$contexts = $modx->getCollection('modContext', array('key:NOT IN' => ['mgr']));

$tpl = $modx->getOption('tpl',$scriptProperties , 'tpl.context_menu');

foreach ($contexts as $context) {
    $ctxArray = $context->toArray();
    $output .= $modx->getChunk($tpl,$ctxArray);
}
return $output;

4) К нему обязателен чанк tpl.context_menu:
<li class="js-context_key [[+key]] [[*context_key:is=`[[+key]]`:then=`active`:else=``]]" data-key="[[+key]]" data-link="[[++site_url]]">[[+name]]</li>

5) Снипет для подтягивания контента из ресурсов оригинального контекста. Можно использовать pdoField опять же от Василия Наумкина, но не было желания ставить излюбленый «швейцарский нож» для открытия банки Колы )).
$res = $modx->getObject('modResource', $id);
$content = $res->get($field);

return $content;
Снипет вызываем так: [[snippet?&id=`[[*id]]`&field=`content`]]

6) JS-скрипт для переключения контекстов:
$(document).ready(function(){
    // установка у актуального города класса active
    $(function(){
        var city = $('#city_menu'),
            city_key = city.data('context'),
            city_active = city.find('.js-context_key.'+city_key);
            
        city_active.addClass('active');
    });
    // настройка и переход на страницы других контентов
    $('.js-context_key').click(function(){
        var link = $(this),
            key = link.data('key'),
            locey = window.location.href,
            context = link.parent().data('context'),
            newlocey;
            
        if ( context == 'web' ) {
            if ( locey.indexOf("https") === true ) {
                console.log('https');
                newlocey = locey.replace('https://', 'https://'+key+'.');
            } else {
                console.log('http');
                newlocey = locey.replace('http://', 'http://'+key+'.');
            }
        } else {
            if ( key == 'web' ) {
                newlocey = locey.replace(context+'.', '');
            } else {
                newlocey = locey.replace(context, key);
            }
        }
        console.log(key+' - key; '+locey+' - locey; '+context+' - context; '+newlocey+' - locey;');
        
        if ( context != key ) {
            location.href = newlocey;
        }
    });
});
Ну и сама html разметка:
<div id="citys_menu">
    <ul id="city_menu" data-context="[[!*context_key]]">
        [[!ContextMenu?]]
    </ul>
</div>
У данного способа есть недостатки:
  • например что не получается, так это восстановить ресурс после помечания на удаление через всплывающее меню (правый клик). Приходиться вызвать быстрое редактирование и там убирая галочку сохранять.
  • переключение работает на js а хотелось бы на php, если вдруг js отключат из-за нехватки ресурсов, кризис на дворе ))
Те самые переменные которые потом изменяются в зависимости от города, прописываем в настройках контекста города.
Юрий Фомин
30 января 2017, 12:13
modx.pro
16
6 409
+11

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

Максим Кузнецов
30 января 2017, 15:29
+2
$response->set('content', '[[!OriginalFields?&id=`'.$id.'`&field=`content`]]');

— выглядит как какое-то извращение..) Лучше так:

$params = array();
$params['id'] = $id;
$params['field'] = 'content';

$snippet_result = $modx->runSnippet('OriginalFields', $params);

$response->set('content', $snippet_result);

UPD: А для чего эта магия, если не секрет?
$id = $id;
$field = $field;
    Юрий Фомин
    30 января 2017, 15:54
    0
    По поводу текста контента, весь фокус в том чтобы не вставлять весь контент в ресурсы контекстов-дублей. Представь что у оригинального контекста статься на 5000 знаков и это все помнож на количество контекстов-дублей (к примеру 80) иного 400000 знаков в базе зачем они там? не лучше ли оставить 5000 у ресурса оригинального контекста, а в поле контекста ресурсов-дублей поставить вызов снипета который и будет вытягивать контент только оригинального ресурса. надеюсь понятно )).
    По поводу
    $id = $id;
    $field = $field;
    лучше у Копирфильда спросить я и сам пока разбираюсь. Вобще тут должны подтягиваться настройки снипета для подтягивания контента из ресурсов оригинального контекста (пункт 5). к примеру [[originalFields?&id=`[[*id]]`&field=`content`]]. Я учусь и буду только рад если кто подскажет как это правильно подтянуть в самом снипете.
      Максим Кузнецов
      30 января 2017, 16:00
      +1
      Первая часть кода задавалась вопросом не что делает этот сниппет и зачем он нужен, а лишь о способе его вызова в php-коде.

      По поводу переменных для самописного сниппета — все параметры вида &param=`value` по-умолчанию доступны внутри сниппета в виде переменных $param.
        Юрий Фомин
        30 января 2017, 16:05
        0
        в том то и дело что в этом плагине он ничего не должен делать, а то он просто подставит весь контент в ресус-дубь. чего и хотелось избежать.
        то есть код
        [[!OriginalFields?&id=`'.$id.'`&field=`content`]]
        должен сработать не в момент работы плагина по редактированию ресурса, а в момент открытия страницы. поэтому от и вынесен в отдельный снипет. в противном случае можно было бы просто указать:
        $response->set('content', $resource->get('content'));
          Максим Кузнецов
          30 января 2017, 16:09
          +1
          … если честно, не совсем понимаю, о каком открытии страницы идет речь.

          Например, для такого вызова плагина ваш сниппет сможет получить данные?
          $modx->invokeEvent('OnDocFormSave',array(
          	'mode' => 'upd',
          	'id' => $page->get('id'),
          	'resource' => &$page,
          ));
            Юрий Фомин
            30 января 2017, 16:18
            0
            про «открытие страницы» все просто:
            оригинал — joxi.ru/4AkOW64upM6nAq
            дубль — joxi.ru/1A598YPcaKezAE

            про ваш плагин, я вобщето еще был бы рад подучить то что вы тут написали. маловато знаний у меня, только учусь. если объясните что и как, отвечу.
            Максим Кузнецов
            30 января 2017, 16:16
            +2
            Если у вас есть единый контент для всех связанных ресуров, содержимое которого хранится у ресурса web-контекста, можно поступить так:

            if ($modx->event->name == 'OnBeforeDocFormSave' && $mode == 'upd') {
            	if ($resource->get('context') != 'web') {
            		$original_page = $modx->getObject('modResource', array(
            			'context_key'=> 'web', 
            			'alias' => $resource->get('alias')
            		));
            
            		if ($original_page) {
            			$resource->set('content', $original_page->get('content'));
            		}
            	}
            }
            — таким способом мы будем подставлять контент основного ресурса при инициализации страницы редактирования побочных.

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

            if ($modx->event->name == 'OnDocFormSave' && $mode == 'upd') {
            	if ($resource->get('context') != 'web' && strlen($resource->get('content')) > 0) {
            		$original_page = $modx->getObject('modResource', array(
            			'context_key'=> 'web', 
            			'alias' => $resource->get('alias')
            		));
            
            		if ($original_page) {
            			$original_page->set('content', $resource->get('content'));
            
            			$resource->set('content', '')
            		}
            	}
            }
              Юрий Фомин
              30 января 2017, 16:23
              0
              не вижу в этом смысла…
              во первых плагин должен выполнить дополнительные манипуляции, что увеличивает время работы (а контекстов у нас уйма).
              во вторых зачем забивать в базу дублями контента, который может быть довольно обьемный.
                Максим Кузнецов
                30 января 2017, 16:41
                +1
                Понял. Ну, плагин выше и не будет плодить в базу дубли контента, он по-прежнему будет один у web'a — у остальных же поле будет очищаться при сохранении.

                Вышеописанный код решает задачу, с помощью которой можно видеть в админке любого дублируемого ресурса оригинальный контент и работать с ним напрямую.

                А сниппет, на мой взгляд, уместнее вынести непосредственно в шаблон:
                [[*context_key:is=`web`:then=`[[*content]]`:else=`[[!OriginalFields?&id=`[[*original_id]]`&field=`content`]]`]]
                  Юрий Фомин
                  30 января 2017, 16:47
                  0
                  у меня не было задачи чтобы с полями ресурсов-дублей велась работа. туда вообще бы никогда бы и не заглядывали. Но ваше решение интересно… надо будет влить в плагин.
                  И уже далее вы правы на счет вывода синпета в шаблон. Но это уже за гранью того что должен был реализовать. Вам огромное спасибо, что помогли.
                    Максим Кузнецов
                    30 января 2017, 16:55
                    +1
                    Да не за что..)

                    Видимо, я все-таки не совсем корректно понял задачу — мне казалось, что ресурсы дублируются во все контексты, т.к. у них потенциально могут быть разные подданные для каждого города. Если же у вас все данные одинаковые и редактировать страницы-дубли никто не будет, то правильнее вообще было бы не плодить лишних страниц, а воспользоваться кастомной маршрутизацией, которая перехватывала бы событие OnPageNotFound и отображала бы страницу-оригинал, доступную по адресу с идентификатором города.
                      Юрий Фомин
                      30 января 2017, 17:05
                      0
                      Интересно, нужно будет на днях изучить и понять. Спасибо.
                        Юрий Фомин
                        30 января 2017, 17:18
                        0
                        Вобще то вы правильно поняли задачу. Поэтому и пришлось делать на контекстах, чтобы в их настройках хранить данные для каждого города отдельно. Дальше бы они подставлялись в контент или в шапку или в контакты… типа:
                        Наш город [[++city]] самый лучший на планете. В нем живут [[++people]] жителей.
            Максим Кузнецов
            30 января 2017, 16:04
            +2
            К слову, о востребованности сниппета — не лучше ли вне цикла получать поле content у основного ресурса один раз, после чего просто передавать переменную?

            //...
            $original_content = $resource->get('content');
            //...
            foreach ($contexts as $context) {
            	//...
            	$response->set('content', $original_content);
            }
            — в таком варианте вы запросите содержимое контента один раз, а не столько, сколько у вас контентов, отличных от web/mgr.
          rrrro
          30 января 2017, 15:47
          0
          А что не так с babel, поделитесь пожалуйста? А то у меня тоже начинается тут один мультиязычный проект. Раньше babel не использовал, поставил-попробовал, вроде нормально работает.
            Юрий Фомин
            30 января 2017, 15:56
            +2
            Для бабела нужна привязка ресурс-ресурс для перелинковки. В моей задаче не должно было быть действий от заказчика вроде «создать перевод ресурса 325», то есть ничего ручками кроме тех преславутых переменных в контенте и по сайту
            для простого мультиязычника бабел вполне хорошая весч.
              rrrro
              30 января 2017, 16:05
              0
              Спасибо.
            Василий Столейков
            30 января 2017, 18:26
            1
            0
            Молодец! Интересное решение! Есть полезные моменты, спасибо что выложил…
              Fi1osof
              31 января 2017, 01:35
              +1
              Синхронизация всего со всем (документы, ТВшки и прочее) в разных контекстах — дикая заморочка. Потом начнется где-то в одном документе отредактировал, а в других не поменялось и т.д. и т.п. Да и при реальном каталоге в 10 000 документов при 10 контекстах иметь 100 000 документов — не очень интересно.
              Прочитайте вот этот комментарий, может идеи возникнут.

              P.S. Я бы так не стал делать, слишком много тонких моментов.
                Юрий Фомин
                31 января 2017, 10:50
                +2
                Спасибо Николай. Я был бы счастлив пройти курс «молодого бойца» модэкс, но где его преподают. Документация на английском, его еще выучить надо. А пока учу английский и мечтаю о курсе пишу исходя из знаний, дабы было что на хлеб намазать )). Если можете подсказать где и как выучить модэкс вдоль и поперек, буду признателен. Мне предлагали перейти на другие движки, но уж нет. С этого мерседеса (не реклама) я не слезу.
                  Fi1osof
                  01 февраля 2017, 06:01
                  +1
                  Не за что.
                  Юрий, я не знаю такого курса. Но информации много и здесь, и у нас на сайте modxclub.ru, и на других ресурсах. Другое дело, что этой информации очень много, и все равно только в процессе работы будет приходить необходимый опыт. Я этот свой комментарий оставил просто для того, чтобы видели, что есть и другие рабочие механизмы в MODX, которые лучше подходят для реализации подобных задач, чтобы знали в сторону чего копать. Но четкого сценария нет, надо в любом случае изучать все это.
                Михаил
                25 апреля 2019, 14:58
                0
                Добрый день
                Вывел контексты в виде меню, но почему то не получается их отсортировать по rank. Контексты отсортированы по key, а это не очень удобно. У меня несколько контекстов-городов. За контекст с названием Москва отвечает web а он получается в самом конце.
                  Сергей
                  12 июня 2020, 11:25
                  0
                  Подскажите пожалуйста как вывести id родителя, основного контекста? Необходимо для ms2gallery &resources=``
                    Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
                    23