Заметки о MODX Revo от новичка

в 17:20, , рубрики: cms, modx, php, заметки на полях, настройка, метки: , , ,

Заметки о MODX Revo от новичка
Disclaimer: Конечно, скорее всего многое, из представленного в этой статье, покажется капитанством для сведующих людей. Однако, возможно, кому-то она поможет...

Введение

Итак, что же такое MODX (кстати, пишется именно так — MODX, а не как название хаба — MODx)? Если читать официальный сайт — то это CMS. Однако, это лишь часть правды. На самом деле, MODX находится примерно посередине между CMS и CMF. Впрочем, любой, кто заинтересовался бы MODX это быстро бы узнал из других статей, поэтому не буду останавливаться на этом пункте подробней.

Поскольку MODX находится посередине между CMS и CMF, то её не так легко освоить, как простую CMS, вроде WordPress или Joomla. Пожалуй, эта статья написана в целях раскрытия некоторых тонкостей, которые кажутся неочевидными на первый взгляд.

Установка

Лично я устанавливал MODX Revo 2.2.4 на сервере, оснащённом nginx и PHP-FPM.
Поскольку вообще MODX заточен под Apache + PHP extension, то для nginx требуется дополнительная настройка. Моя конфигурация выглядит примерно так:

nginx конфигурация

server {
    listen 80;
    server_name example.com *.example.com; #при такой настройке MODX будет обрабатывать в том числе и поддомены сайта.
    index index.php index.html;
    root /srv/http/example.com/public;
    access_log /srv/http/example.com/logs/http_access.log main buffer=50k;
    error_log /srv/http/example.com/logs/http_error.log;

    # Отключаем логирование для robots.txt. Зачем нам информацию кто смотрел файл?
    location = /robots.txt  {
        access_log off;
        log_subrequest off;
        log_not_found off;
    }

    # Отключаем логирование для favicon.ico
    location = /favicon.ico {
        access_log off;
        log_subrequest off;
        log_not_found off;
    }

    # Отключаем логирование для sitemap.xml
    location = /sitemap.xml {
        access_log off;
        log_subrequest off;
        log_not_found off;
    }

    # Отключаем логирование для *.css И *.js файлов
    location ~* ^.+.(css|js)$ {
        access_log off;
        log_subrequest off;
        log_not_found off;
    }

    # Также отключаем логи для картинок, файлов
    location ~* ^.+.(bmp|gif|jpg|jpeg|ico|png|swf)$ {
        access_log off;
        log_subrequest off;
        log_not_found off;
    }

    # Блокируем доступ для всех скрытых файлов, ведь не хотим, чтобы увидели .htaccess, .git, .svn и т.д.
    location ~ /. {
        deny  all;
    }

    # А этот код нужен уже непосредственно для MODX. Если в файловой системе нет запрошенного файла (или папки), используем реврайт (чуть ниже сам реврайт). Мы ведь не хотим, чтобы картинки обрабатывались MODX-ом?
    location / {
        try_files $uri $uri/ @rewrite;
    }

    # Сам реврайт. Всё очень просто - MODX обрабатывает всё с помощью index.php?q= - поэтому просто мы путь туда и перекидываем.
    location @rewrite {
        rewrite ^/(.*)$ /index.php?q=$1;
    }

    # Подключаем обработчик php-fpm
    location ~ .php$ {
        # php-fpm. Подключение через сокет.
        fastcgi_pass   unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_index  index.php;
        fastcgi_param  DOCUMENT_ROOT  /srv/http/example.com/public; # то же самое, что в root
        fastcgi_param  SCRIPT_FILENAME  /srv/http/example.com/public$fastcgi_script_name; # то же самое, что в root + $fastcgi_script_name
        include fastcgi_params;
        fastcgi_param  REMOTE_ADDR $remote_addr; # нужно, чтобы IP запросившего в PHP не был localhost-ом.
    }
}

На самом деле данный конфиг, я просто уверен, что неидеален, но для начальной конфигурации, наверное, подойдёт. Остановиться здесь можно всего на двух вещах, которые, собственно, и подключают MODX (дабы корректно работал ЧПУ):

    server_name example.com *.example.com; #при такой настройке MODX будет обрабатывать в том числе и поддомены сайта.

    # А этот код нужен уже непосредственно для MODX. Если в файловой системе нет запрошенного файла (или папки), используем реврайт (чуть ниже сам реврайт). Мы ведь не хотим, чтобы картинки обрабатывались MODX-ом?
    location / {
        try_files $uri $uri/ @rewrite;
    }

    # Сам реврайт. Всё очень просто - MODX обрабатывает всё с помощью index.php?q= - поэтому просто мы путь туда и перекидываем.
    location @rewrite {
        rewrite ^/(.*)$ /index.php?q=$1;
    }

Собственно этого, вроде бы, достаточно для корректной работы как MODX, так и для всего остального (подгрузки nginx-ом CSS, JS, картинок и прочего без участия PHP).
Сама установка очень проста, достаточно распаковать файлы в нужную папку да вбить в адресной строке example.com/setup/, после чего загрузится простой инсталлятор.

ЧеловекоПонятные URL (ЧПУ)

По умолчанию, в MODX отключен ЧПУ. Включается он очень просто: Система -> Настройки системы -> в фильтр вбиваем «friendly_urls» и нажимаем Enter (можно конечно и ручками найти, но так быстрее) -> Переключаем значение в «Да» и нажимаем «Сохранить» наверху.
Вообще, не забываем, что в MODX кнопка сохранить находится в ВЕРХНЕМ ПРАВОМ УГЛУ. Иногда про неё можно забыть, и тогда настройки могут потеряться — ибо в MODX-е иногда срабатывает автосейв, а иногда и нет.
Аналогично делаем для функции «automatic_alias» для автоматической генерации ЧПУ.
Мы благополучно включили ЧПУ, и теперь ссылки будут формироваться следующим образом:

example.com/resourcename.html

Обратите внимание, что в конец подставляется .html! Это зависит от Content-Type-а каждого отдельного ресурса (изменяется в свойствах самого ресурса). Сами настройки Content-Type-ов можно найти в Система -> Типы содержимого.
Однако, все дополнительные GET параметры всё равно остаются прежними, и работает это примрено так:

example.com/news.html?date=05092012&nid=1

Лично мне такая система не понравилась, и я стал искать способы того, как эти параметры сделать частью ЧПУ. И нашёл!
Для начала, стоит немного рассказать про различные элементы, позволяющие расширять MODX. Их несколько:

  • Template Variables — дополнительные поля свойств ресурсов. К ним вернёмся немного позже
  • Chunks — блоки HTML кода (с возможной MODX разметкой), которые можно вставить в любой друкой чанк, ресурс или вообще куда угодно. Внимание! PHP сюда вставлять нельзя!
  • Snippets — блоки PHP кода, которые выводят некие данные туда, куда они вставляются (при помощи MODX-разметки). Своеобразный PHP-аналог чанков.
  • Plugins — блоки PHP-кода, выполняемые в соответствии с определёнными событиями, происходящими внутри MODX. Чем-то напоминает Drupal-овские хуки, пожалуй.

В данном случае нам понадобятся сниппеты и плагины.
Итак, как же встроиться в MODX со своим форматом URL? на самом деле, очень просто — позволить MODX попробовать найти страницу по данному URL, и когда он её не найдёт — перехватить управление и распарсить, направив после этого MODX туда, куда надо. Для этого необходимо использовать плагин на событие OnPageNotFound.

Код плагина для "улучшенного" ЧПУ

<?php
if($modx->request->getResourceMethod()!="alias")
	return;
$uri = $modx->resourceIdentifier;
$uriChunks = explode("/", $uri);
$paramNum=0;
$uriChunksCount = count($uriChunks);
$paramDelimiter="-"; //DO NOT use "/"! Set the desired delimiter here.
for($i=$uriChunksCount-1;$i>0;$i--)
{
	$parameter=explode($paramDelimiter, $uriChunks[$i], 2);
	if(count($parameter)!=2)
		return;
	$modx->request->parameters['GET'][$parameter[0]]=$parameter[1];
	if(empty($modx->request->parameters['POST'][$parameter[0]]))
		$modx->request->parameters['REQUEST'][$parameter[0]]=$parameter[1];
	$paramNum++;
	unset($uriChunks[$i]);
	$uri = implode("/", $uriChunks);
	if (array_key_exists($uri, $modx->aliasMap))
	{
		$modx->sendForward($modx->aliasMap[$uri]);
	}
}
if($paramNum==($uriChunksCount-1))
{
	$parameter=explode($paramDelimiter, $uriChunks[0], 2);
	if(count($parameter)!=2)
		return;
	$modx->request->parameters['GET'][$parameter[0]]=$parameter[1];
	if(empty($modx->request->parameters['POST'][$parameter[0]]))
		$modx->request->parameters['REQUEST'][$parameter[0]]=$parameter[1];
	$modx->sendForward($modx->getOption('site_start', null, 1));
}

Собственно, здесь пожалуй стоит остановиться на следующих вещах:

$modx->request->getResourceMethod()

Проверка на то, включен ли вообще ЧПУ.

$modx->resourceIdentifier

В этой не столь очевидной переменной хранится запрашиваемый URL.

$paramDelimiter="-";

Эта переменная задаёт разделитель между именем GET-параметра и его значением. Можете менять на что угодно, кроме "/".

$modx->request->parameters['GET'][$parameter[0]]=$parameter[1];
if(empty($modx->request->parameters['POST'][$parameter[0]]))
	$modx->request->parameters['REQUEST'][$parameter[0]]=$parameter[1];

Дело в том, что при каждом запросе MODX сохраняет всю информацию о GET и POST параметрах в свои собственные массивы, после чего формирует общий массив примерно таким способом:

$modx->request->parameters['REQUEST'] = array_merge($modx->request->parameters['GET'], ($modx->request->parameters['POST']);

Для сохранения данной структуры мы вынуждены каждый раз проверять, не сохранено ли в REQUEST-массиве данные из POST-массива, и только при отсутствии таковых заменять данные в REQUEST-массиве.

if (array_key_exists($uri, $modx->aliasMap))
{
	$modx->sendForward($modx->aliasMap[$uri]);
}

А это собственно перенаправление на нужный ресурс. В $modx->aliasMap хранится как раз вся таблица ЧПУ, и с ней мы и сверяемся, выясняя, не пора ли нам уже переходить к нужному ресурсу, или же содержимое нашего недопарсенного uri до сих пор состоит из параметров. $modx->sendForward и осуществляет оное перенаправление.

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

<?php
$currentURL="";
foreach($modx->request->getParameters() as $key => $value)
{
	$currentURL.=$key."=".$value."&";
}
$currentURL = $modx->makeUrl($modx->resource->get("id"), $modx->context->get("key"), $currentURL);
return $currentURL;

Собственно, особо и объяснять нечего. $modx->makeUrl как раз и формирует URL. Для справки — формирует он его относительно контекста — второй параметр как раз задаёт контекст.

Мультисайтовость в MODX

Обработка нескольких доменов разными контекстами одним MODX-ом

Собственно, эта тема уже поднималась, например, в данном топике и на просторах интернета. Я бы хотел представить своё решение, как мне кажется, достаточно простым. Но для начала, перечислю основные методы решения этой проблемы:

  1. правка index.php. В index.php в явном виде указано, какой именно контекст загружается при старте. Его можно вполне подправить, и подставить туда свои условия с выбором своего контекста. Почему этот метод мне НЕ нравится: правка оригинальных файлов modx. А почему мне не нравится этот факт? Сложность при обновлениях. За что я люблю modx (и столь же сильно не люблю форумный движок SMF), это за то, что в нём НЕ ТРЕБУЕТСЯ менять никакие файлы оригинального MODX, что просто настолько облегчает обновление движка, насколько это только возможно.
  2. создание плагина на событие OnHandleRequest (общий недостаток: используется чуть больше ресурсов — сначала всегда загружается контекст web, и только после этого загружается нужный контекст):
    1. Аналогично правки index.php, но без его правки. Всё почти то же самое — жёстко задаётся список доменов, которые обрабатываются жёстко заданным списком контекстов. Как вариант название контекста содержит в себе часть названия домена (контекст forum для поддомена forum.example.com например), на основании чего и выполняется поиск нужного контекста Для простых сайтов подойдёт, но гибкости не хватает.
    2. Сканирование настроек контекстов с поиском нужного. Максимальная гибкость, автосоздание контекстов, отсутствие необходимости править плагин после его создания.

Я собираюсь здесь описать как раз последний вариант. Мой текст плагина (напоминаю, событие OnHandleRequest):

Плагин-gateway

<?php
if($modx->context->key!="mgr")
{
	$object = $modx->getObject('modContextSetting', array('key' => 'multisite_http_host', 'value' => $modx->getOption('http_host')));
	if($object)
		$modx->switchContext($object->get('context_key'));
}

Для использования необходимо в свойствах контекста проставить параметр «multisite_http_host» со значением того, какой домен он обрабатывает. Если плагин не нашёл ни одного подходящего контекста, он использует по умолчанию web-контекст.
Здесь я хочу сделать акцент на двух вещах.
Во-первых, для того, чтобы успешно использовать MODX, необходимо понять принцип xPDO. xPDO — механизм для связи между базой данных и объектами PHP. Работая с объектом в PHP мы получаем доступ к базе данных. Фактически всё в MODX использует xPDO для связи с БД.

$object = $modx->getObject('modContextSetting', array('key' => 'multisite_http_host', 'value' => $modx->getOption('http_host')));

Данный код как раз и обращается в базу данных для поиска объекта настроек контекста, содержащие определённые условия (а именно, ключ настройки «multisite_http_host» со значением нашего http host-а). После чего мы и получаем ключ контекста, который нам необходимо загрузить, уже из полученного объекта ($object->get(«context_key»)).
Во-вторых, хабрапользователь XanderBass задал правильный вопрос, ответ на который я и хотел бы здесь продублировать. Итак, почему же я использую имя параметра «multisite_http_host», а не просто «http_host»?
Всё очень просто, это следует из механизма совмещения настроек системы, контекста и пользователя. Дело в том, что настройки системы затираются настройками контекста, а те, в свою очередь, настройками пользователя.
В данном случае, http_host является настройкой системы. Если бы я использовал в настройках контекста http_host вместо multisite_http_host, то она была бы затёрта настройкой контекста. Конечно, в данном конкретном примере это не столь страшно. Но достаточно лишь чуть-чуть переписать данный плагин для обработки всех поддоменов данного домена одним контекстом! Или же, сделать обработку одним контекстом нескольких разных доменов (например, вбивая в параметр примерно в таком формате: sub1.example.com;sub2.example.com;sub3.example.com" и т.п.). В таком случае, в http_host располагался бы не http_host, используемый при запросе, а именно эта самая настройка. А ведь мало ли что, возможно, нам понадобится оригинальный http_host где-нибудь ещё…

Аутентификация на нескольких контекстах

Иногда хочется аутентифицировать пользователя сразу на нескольких контекстах. По умолчанию, пользователь, зашедший в систему (например, при помощи мода Login), заходит в систему лишь на одном единственном контексте. Следующий плагинпри авторизации автоматически заходит на несколько контекстов (события OnWebLogin и OnWebLogout):

Мультиаутентификация

<?php
$currentSiteGroup = $modx->getOption("multisite_site_group");
if(empty($currentSiteGroup)) return;
$currentContext = $modx->context->get("key");
$currentContextSettings = $modx->getCollection('modContextSetting', array('key' => "multisite_site_group", "value" => $currentSiteGroup));
foreach($currentContextSettings as $currentContextSetting)
{
	$contextKey = $currentContextSetting->get('context_key');
	if($contextKey!="mgr" && $contextKey!=$currentContext)
	{
		if($user)
		{
			if($modx->event->name=="OnWebLogout")
			{
				$modx->user->removeSessionContext($contextKey);
			}
			else if($modx->event->name=="OnWebLogin")
			{
				$modx->user->addSessionContext($contextKey);
				$_SESSION['modx.'.$contextKey.'.session.cookie.lifetime']=$attributes["lifetime"];
			}
		}
	}
}

Для использования необходимо проставить у всех контекстов, на которые вы хотите чтобы одновременно заходил пользователь, параметр «multisite_site_group» со значением, одинаковым для всех нужных контекстов.
Всего несколько комментариев:
Здесь поиск нужных контекстов осуществляется очень похоже на то, как мы осуществляли их в предыдущем плагине, разница только в использовании getCollection вместо getObject. Делая так, мы получаем массив объектов, которые следуют указанным условиям.

$modx->user->addSessionContext($contextKey);

и

$modx->user->removeSessionContext($contextKey);

как раз и осуществляют авторизацию пользователя на том или ином контексте.

$_SESSION['modx.'.$contextKey.'.session.cookie.lifetime']=$attributes["lifetime"];

проставляет «время жизни» авторизации для всех остальных контекстов таким же, как и для текущего.

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

Автор: Evengard

Поделиться

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