Доброго здравия всем хабражителям!
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. Из-за ограничений по нагрузке. Последовательность моих действий.
- На старом сайте открываем на редактирование файл экспорта. Ставим на дальнейшее действие «продолжить».
- На новом сайте открываем на редактирование файл, куда мы переносим контент (далее файл импорта). Меняем шаблон на шаблон импорта из JSON, сохраняем.
- В параметрах файла импорта выставляем текущий контекст, URL файла экспорта, сопоставление шаблонов и TV-параметров. Сохраняем.
- В файле экспорта меняем значение в startID на id родительского ресурса, откуда будем экспортировать контент. Сохраняем.
- В файле импорта выставляем id ресурса, куда будем импортировать. Сохраняем.
- Вызываем файл импорта на просмотр. Далее повторяем, пока в конце не появится ссылка с надписью «Start»:
- Ждём, пока загрузка завершится.
- Нажимаем на ссылку «Import next items»
- После того, как импортировали всё из нужного ресурса, возвращаемся к пункту 4, если ещё что-то нужно импортировать.
Да, знаю, для большей производительности можно было бы делать всё прямыми запросами к БД. Только, во-первых, не факт, что это исправило бы ситуацию с 502-й ошибкой. Во-вторых, не было времени изучать что затрагивается в БД при создании ресурса, кроме site_content. В-третьих, написал бы такое решение, меня бы тут же запинали бы с формулировкой «а-как-же-XPDO».
Ещё раз напоминаю, что это лишь предварительный набросок решения. Всем спасибо за внимание к моему очередному велосипеду!
Автор: XanderBass