Заготовка «Импорт-экспорт»

в 23:39, , рубрики: modx, перенос, экспорт, метки: , ,

Доброго здравия всем хабражителям!

MODx Revolution удобна во многих отношениях. Если в MODx Evolution можно было сделать всё, то в MODx Revolution можно сделать абсолютно всё. Были бы фантазия и терпение. Однако, после появления Revolution у многих встал вопрос: каким образом перетащить содержимое с одного движка на другой. Одно дело, если у Вас с десяток ресурсов. Тут копипаста Вам в помощь. Другое дело — коллекции контента, каталоги и прочее подобное.

Предыстория

Было у меня две коллекции — анекдотник и былинник. В первой я собирал любимые анекдоты, во второй — истории с «ЯПлакалъ», «IThappens» и прочих интересных порталов. Всё это висело на Evolution 1.0.5. Однако, в один прекрасный день я перевёл весь свой много-доменный сайт на один движок и одну БД. В общем перешёл на Revolution. Естественно встал вопрос о переносе контента. С разделом «о себе» и музыкальным разделом всё было просто — копипаста. О форуме я вообще не парился — он всё равно на phpBB. А вот с анекдотником и былинником вопрос пришлось отложить в долгий ящик, ибо скопипастить всё накопившееся там терпения не хватило бы…

Экспорт

На старом сайте жил малюсенький сниппет импорта случайного анекдота с анекдотника. По сути анекдотник мог экспортировать данные. В последствии я сделал специальную страничку, которая экспортировала всё содержимое сайта в формат JSON, да и забыл про неё. Когда встал вопрос о переносе данных вспомнил именно о ней.

Почему JSON? Да просто, наверное, потому, что устал я чертовски от всяких XML-парсеров. Даром, что для JSON существует простейшие функции - json_encode и json_decode. Это чрезвычайно удобное обстоятельство делает вариант с JSON куда более предпочтительнее, чем все остальные варианты.

С экспортом в JSON всё просто. Итак содержимое страницы для экспорта (шаблон _blank):

{"items":[
[[Ditto? &startID=`162` &tpl=`cat` &tplLast=`catLast`]]
]}

Содержимое чанка cat:

    {
      "name":"[+pagetitle+]",
      "alias":"[+alias+]",
      "template":"[+template+]",
      "hidemenu":"[+hidemenu+]",
      "content":[
[!Ditto? &startID=`[+id+]` &tpl=`item` &tplLast=`itemLast`!]
      ]
    },

catLast — то же самое, только без запятой в конце. Содержимое чанка item:

        {
          "name":"[+pagetitle+]",
          "alias":"[+alias+]",
          "template":"[+template+]",
          "hidemenu":"[+hidemenu+]",
          "content":"[+content:strip:noquotes+]"
        },

itemLast — то же самое, только без запятой в конце.

В итоге получается внушительный такой файлик. Да, главное — не забыть выставить тип данных на странице экспорта. Тип данных - text/javascript. Каким-то макакером можно сразу экспортировать данные Ditto в JSON. Но времени разбираться в этом вопросе не было и нет.

Импорт

Файлик получили. Что дальше? А дальше я наткнулся на статейку о создании соц-сети на MODx и узрел, как именно программным путём можно создать в MODx Revolution новые документы. Родилась идея, а вслед за ней сниппет:

<?php
// Импорт из JSON-файла
// Функция отвечающая за добавление ресурса
function addItem($ctx,$pagetitle,$template,$isfolder,$hidemenu,$parent,$alias,$content,$td){
  global $modx;

  $newResource = $modx->newObject('modResource');

  $newResource->fromArray(array(
    'pagetitle'=>$pagetitle,
				'longtitle'=>$pagetitle,
				'content'=>$content,
				'template'=>$template,
				'isfolder'=>$isfolder,
				'hidemenu'=>$hidemenu,
				'parent'=>$parent,
				'published'=>'1',
				'alias'=>$alias,
				'context_key'=>$ctx
		));

  if ($newResource->save()) {
    $id = $newResource->get('id');
    $modx->cacheManager->refresh();
				$modx->reloadConfig();

    if (is_array($td)) {
      foreach($td as $key=>$val) {
        $tvar = $modx->newObject('modTemplateVarResource');
        $tvar->set('contentid',$id);
        $tvar->set('tmplvarid',$key);
        $tvar->set('value',$val);
        $tvar->save();
      }
				}

    return $id;
  } else { return false; }
}
// Функция, отвечающая за рекурсивную обработку массива с данными
function handleItem($ctx,$item,$parent,$tpls,$tvs,$handleChildren=false){
  $hidm = isset($item['hidemenu'])?$item['hidemenu']:'0';
		$isf = is_array($item['content'])?'1':'0';
		$content  = is_array($item['content'])?'':$item['content'];
		$tpl = array_key_exists('tpl'.$item['template'],$tpls)?$tpls['tpl'.$item['template']]:'0';
  $td = array();
		foreach($tvs as $tvn=>$tvv) if (array_key_exists($tvn,$item)) $td[$tvv] = $item[$tvn];
  $ret = '';
		if ($id = addItem($ctx,$item['name'],$tpl,$isf,$hidm,$parent,$item['alias'],$content,$td)) {
    $ret = 'Resource «<b>'.$item['name'].'</b>» imported successfully! '
				     . 'New ID: <b>'.$id.'</b><br />';
				if (is_array($item['content']) && $handleChildren)
				  foreach ($item['content'] as $i) $ret.= handleItem($ctx,$i,$id,$tpls,$tvs,$handleChildren);
				return $ret;
		} else { return 'Resource «<b>'.$item['name'].'</b>» not imported!<br />'; }
}
// Шапкама лога
$cons = '<h1>Import item log</h1>';
// Количество импортируемых за один раз элементов (для не сильно производительных систем)
$item_count = isset($itemCount)?$itemCount:4;
// Контекст, куда всё импортируется
if (!isset($curContext)) $curContext = 'web';
// "Запоминалка" следующих элементов к импорту (для не сильно производительных систем)
$next_items = isset($_GET['jsonimportnext'])?intval($_GET['jsonimportnext']):0;
// Сопоставление шаблонов
$tpls = array();
if (isset($templates)) {
  $tmp = explode(',',$templates);
		foreach($tmp as $val) {
    $tpls_d = explode('=>',$val);
				$tpls['tpl'.$tpls_d[0]] = $tpls_d[1];
		}
}
// Сопоставление TV-параметров
$tvs = array();
if (isset($tvParams)) {
  $tmp = explode(',',$tvParams);
		foreach($tmp as $val) {
    $tvs_d = explode('=>',$val);
				$tvs[$tvs_d[0]] = $tvs_d[1];
		}
}
// Сам процесс
if (isset($source) && isset($rootID)) {
  if ($import_content = @file_get_contents($source)) {
  		$import_data = json_decode($import_content,true);
				$import_count = count($import_data['items']);
				if ($item_count != 0) {
  				for($c = 0; $c < $item_count; $c++) {
        $n = $item_count*$next_items+$c;
				  		if (isset($import_data['items'][$n]))
								  $cons.= handleItem($curContext,$import_data['items'][$n],$rootID,$tpls,$tvs);
				  }
						$this_res = $modx->resource->get('alias');
						$this_res.= '.html';

						if (($item_count*$next_items+$item_count-1)<$import_count) {
    				$cons.= '<br /><a href="'.$this_res.'?jsonimportnext='
								      . ($next_items+1).'">'
		  				      . 'Import next items</a><br />';
						} else { $cons.= '<br /><a href="'.$this_res.'">Start</a>'; }
				} else {
				  foreach ($import_data['items'] as $item)
				    $cons.= handleItem($curContext,$item,$rootID,$tpls,$tvs,true);
				}
		} else { $cons.= 'Cannot get source!<br />'; }
} else { $cons.= 'Invalid execution parameters!<br />'; }

return $cons;

Сразу скажу: это не претендует на универсальное решение. Код практически не откомментирован, увы. Слишком спешил поделиться с нуждающимися. Если решение покажется интересным, буду продолжать развивать работу и возможно создам полноценную надстройку над MODx.

На вход сниппет получает следующие параметры:

  • source (обязательно) — источник JSON файла.
  • itemCount — количество импортируемых элементов за один проход (для не сильно производительных систем). По умолчанию — 4. Если выставить в 0, обрабатываться будет всё за один раз, притом рекурсивно.
  • templates — сопоставление шаблонов. Через запятую перечисляются сопоставления в формате old_id=>new_id, где old_id — id шаблона старого сайта, new_id — id шаблона нового сайта. Если парсер не находит сопоставления, выставляется шаблон 0 (пустой).
  • tvParams — сопоставление TV-параметров. Через запятую перечисляются сопоставления в формате old_name=>new_id, где old_name — имя переменной старого сайта, new_id — id переменной нового сайта. Если парсер не находит сопоставления, переменная пропускается.
  • curContext (обязательно) — текущий контекст. В принципе, если не выставить контекст, тогда он установится в «web».
  • rootID (обязательно) — id ресурса, куда будут импортироваться документы.

К чему разговоры о производительности. А дело в том, что когда я запустил ещё первую версию сниппета, где рекурсивно должно было обрабатываться всё, сервер мне выдал 502-ю ошибку. Проще говоря хостер зарубил высокую нагрузку. Ещё бы — там столько документов было.

Как пользоваться

Для начала пишем простенький шаблон:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru"><head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  <base href="/" />
  <title>[[*pagetitle]]</title>
		<style type="text/css">
body { font: 12px monospace; }
		</style>
</head><body><div align="center"><div style="text-align: left; width: 800px;">
  [[!importJSON? &source=`[[*sourceURL]]` &itemCount=`6` &templates=`[[*templatesReplace]]` &tvParams=`[[*tvsReplace]]` &curContext=`[[*currentContext]]` &rootID=`[[*importDestination]]`]]
</div></div></body></html>

Затем создаём и привязываем к шаблону TV-параметры sourceURL, templatesReplace, tvsReplace, currentContext, importDestination. Не надо материться на currentContext и вещать мне про context_key. В теории Вы можете создать одну страницу и импортировать данные в разные контексты. Собственно всё. В дополнение скажу, как использовал эту штуку я. Сразу сделаю примечание, что я в шаблоне экспорта обходился без категорий, меняя каждый раз startID. Из-за ограничений по нагрузке. Последовательность моих действий.

  1. На старом сайте открываем на редактирование файл экспорта. Ставим на дальнейшее действие «продолжить».
  2. На новом сайте открываем на редактирование файл, куда мы переносим контент (далее файл импорта). Меняем шаблон на шаблон импорта из JSON, сохраняем.
  3. В параметрах файла импорта выставляем текущий контекст, URL файла экспорта, сопоставление шаблонов и TV-параметров. Сохраняем.
  4. В файле экспорта меняем значение в startID на id родительского ресурса, откуда будем экспортировать контент. Сохраняем.
  5. В файле импорта выставляем id ресурса, куда будем импортировать. Сохраняем.
  6. Вызываем файл импорта на просмотр. Далее повторяем, пока в конце не появится ссылка с надписью «Start»:
    1. Ждём, пока загрузка завершится.
    2. Нажимаем на ссылку «Import next items»
  7. После того, как импортировали всё из нужного ресурса, возвращаемся к пункту 4, если ещё что-то нужно импортировать.

Да, знаю, для большей производительности можно было бы делать всё прямыми запросами к БД. Только, во-первых, не факт, что это исправило бы ситуацию с 502-й ошибкой. Во-вторых, не было времени изучать что затрагивается в БД при создании ресурса, кроме site_content. В-третьих, написал бы такое решение, меня бы тут же запинали бы с формулировкой «а-как-же-XPDO».

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

Автор: XanderBass

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js