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