Скрытый потенциал MODX

в 21:05, , рубрики: modx, modx revolution, сниппеты, сплит-тестирование, метки: , ,

Честно говоря у меня противоречивые мнения относительно всех возможностей которые предоставляет MODX Revolution. Но факт, что эта CMF обладает огромнейшим потенциалом нельзя скрыть. Многим веб-разработчикам данная информация может показаться неинтересной в виду того, что у меня специфичные задачи. Кому-то — эта информация покажется интересной, но не более того, т. к. на практике применять врятли придется. Что ж, в любом случае я расскажу свою историю а там мало ли…

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

Для начала поставим задачу. Итак, имеется несколько доменов. Каждый домен это набор страниц независимых друг от друга, т. к. нет меню, списка статей, авторизации и т. п. Стандартных решений. т. е. 1 страница это конкретное описание чего-либо: подписка на рассылку, описание товара, письма которые получают подписчики и т.п. Но есть одно существенное НО — на этих самых самостоятельных страницах по принципу кольца предлагается какой-либо товар характеризующийся таким параметрами, как: цена, ссылка на оформление заказа, фотография товара. Периодически значения данных параметров меняются (цена по акции, новая версия товара, цена для сплит-теста и т. д.).

Сразу же приходит на ум следующее решение

Решение 1. Выделяется один основной домен (акцептор) на котором создается каталог товаров. Акцептор не обязательно, афишировать. Он может быть и техническим. В нужных местах с доноров вставить запрос данных с акцептора. PROFIT.

Это стандартное решение, которые приходит на ум, но т. к. у нас все-же речь идет про MODX и я очень хотел попробовать мультисайтовость в revolution ветке, то продумал следующее решение:

Решение 2. Создам базу в которой буду хранить свой mini каталог товаров. А далее, банальным select … from … where … достану в нужный момент данные. По сути это решение производное от первого решения, но все-же немного другое. Помимо всего прочего, при мультисайтовости в revolution можно было использовать одни и картинки/шаблоны многократно.

Начало реализации 2 решения стандартное: контексты → параметры контекстов → создание часто используемых шаблонов → загрузка картинок и создание документов.

Когда дело дошло до создания той самой базы пришло понимание — с наскоку свой компонент я не напишу. Тем более, для решения этой задачи у меня было всего пару часов. Поэтому немного нервно покурив решил воспользоваться наборами параметров.

Стандартный и «правильный» подход тут следующий: для товара Х создается набор Х с параметрами A,B,C. Затем создается сниппет GetParam следующего содержания

<?php
if(isset($$key)){
	return $$key;
}

к которому добавляется тот самый набор Х. Затем, в нужном месте страницы вставляется вызов [[!GetParam@X? &key=`A`]]

Все работает как нужно, но т. к. проект делается для себя. И в будущем, когда будет готов компонент не хочется тратить время на переделывание таких вызовов для работы со своей базой. Поэтому я набросал сниппет извлекающий данные через объекты. Таким образом вызов у меня теперь выглядит так:

[[!GetParam? &id=`X` &param=`A`]]

<?php
$id=isset($id)?$id:'';
$param=isset($param)?$param:'';
if($id!='' && $param!=''){
	$propSet = $modx->getObject('modPropertySet',array('name'=>$id));
	if($propSet!==NULL){
			$value = $propSet->getProperties();
			return isset($value[$param])?$value[$param]:'';
	}
}
return '';

Пока я разбирался с этой основной задачей нашел «элегантный» способ организовать сплит-тест 2 страниц средствами движка. Знатоки revolution уже наверное догадались, что речь пойдет про документы типа «символическая ссылка». И как можно догадаться, в то поле, куда вписывается ID страницы я вставил вызов сниппета.

[[!Random? &list=`86,49` &cookie=`vsapns`]]

<?php
$list=isset($list)?explode(',',$list):array();
$id='';
if(isset($cookie) && isset($_COOKIE[$cookie])){
  $id=$_COOKIE[$cookie];
}
if($id==''){
   $id=array_rand($list);
			if(isset($cookie)){
			   setcookie($cookie,$id,time()+365*24*3600);
			}
}
return $list[$id];

Все бы здорово, но данный подход приемлем только в случае, если у нас шаблон «символической ссылки» совпадает с шаблоном реального документа. А это невозможно, если сравниваются 2 документа абсолютно разные по дизайну. Поэтому в некоторых случаях приходилось обходиться всем известным типом ресурса «ссылка». Соответственно вызов сниппета меняется на [[~[[!Random? &list=`86,49` &cookie=`vsapns`]]]]

Но тут есть маленькая хитрость. На вкладке настройки у документа типа «ссылка» появляется поле в котором указан header отправляемый при запросе этого документа. По умолчанию там написано HTTP/1.1 301 Moved Permanently. Меняем его на HTTP/1.1 307 Temporary Redirect. Это необходимо для того, чтобы по завершению сплита пользователь принял участие в новом сплите. А не редиректился на старую оттестированную страницу.

Статья получается длинная, а практических советов мало. Поэтому без лирики перейдем сразу к favicon.ico. Всем известно, что если на странице html коде явно не указан адрес иконки, то браузер ее попытается загрузить по адресу /favicon.ico Есть решение, если не хочется отдавать 404 ошибку или одну и ту же иконку для всех сайтов.

Алгоритм следующий

  • Создаем в админке новый тип содержимого ICO с расширением файла .ico и MIME типом image/x-icon
  • В нужном контексте создаем новый статический ресурс
    1. Местонахождение содержимого: встроенный
    2. Тип содержимого: ICO
    3. Тип ресурса: статичный ресурс
    4. Псевдоним: favicon
    5. Статический ресурс: указываем путь к нужной favicon

Таким образом, по адресам test1.example.com/favicon.ico и test2.example.com/favicon.ico мы получаем разное содержимое. Аналогично делается и с robots.txt. Только там тип ресурса выбирается text.

т. к. шаблонов для страниц и картинок у нас не много и они используются на всех доменах в данной инсталляции было решено грузить всю статику с определенного домена. Для этого я набросал небольшой плагин к которому необходимо создать параметр StaticNewUrl со значением домена с которого будем грузить статику.

Код плагина

if($modx->event->name=='OnWebPagePrerender'){
    $html = &$modx->resource->_output;
    $replaceD=array();
	preg_match_all('#src=(?:"|')(.*?)>#',$html,$matches);
	foreach($matches[1] as $item){
		if(substr($item,0,1)!='/'){
			continue;
		}
		if(substr($item,0,2)!='//'){
			$replaceD[md5($item)]=$item;
		}
	}
	preg_match_all('#<link.*?href=(?:"|')(.*?)>#',$html,$matches);
	foreach($matches[1] as $item){
		if(substr($item,0,1)!='/'){
			continue;
		}
		if(substr($item,0,2)!='//' && substr($item,0,12)!='/favicon.ico'){
			$replaceD[md5($item)]=$item;
		}
	}
	array_unique($replaceD);
	foreach($replaceD as $item){
		$html=str_replace($item,$StaticNewUrl.substr($item,1),$html);
	}
}

Ну а раз взялись за оптимизацию загрузки статики, то грех не перенести с evolution ветки

плагин вытягивающий контент в 1 строчку.

if($modx->event->name=='OnWebPagePrerender'){
  $flag=true;
  if(isset($tvHtmlInLine) && (int)$tvHtmlInLine>0){
     $id = $modx->resource->get('id');
     $tvs = $modx->getObject('modTemplateVarResource',array('tmplvarid'=>(int)$tvHtmlInLine, 'contentid'=>$id));
     if($tvs && 0==$tvs->get('value')){
           $flag=false;
     }
  }
  if($flag){
      $html = &$modx->resource->_output;
      $html = preg_replace('|s+|', ' ', $html);
      $html = str_replace('> <','><',$html);
  }
}

Правда с установкой тут все немного сложнее, т. к. придется создать TV параметр. Хочу обратить внимание, что плагин вытягивает контент и на страницах с шаблоном blank. Поэтому для целей, где используется данный шаблон и нельзя вытягивать контент в 1 строку (файл robots.txt, например) я создаю новый одноименный шаблон с содержимым [[*content]]. В общем кому интересны подробности — велком в личку или спрашивайте прямо в комментариях.

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

© 2000—2012

<?php
//Example: [[Copyright? &date=`2010` &sep=`-`]]
if(!isset($time)){
   $time=time();
}
$now=date($format,$time);
$out='';
if(isset($date) && $date!=$now){
 $out.=$date;
 if(isset($sep)){
    $out.=$sep;
	}
}

$out.=$now;
return $out;

Автор: Agel_Nash

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


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