Добрый день. Вы vapor с официального сайта качали или из репозитория modx.com качали мой vapor?
Доброго дня. После установки пакета собранного посредством vapor требуется переустанавливать некоторое расширения, например ACE, Redirector, MIGX и д.р. Т.е. развернутая сборка требует перепроверки, что отнимает, опять же, время. Как считаете, в чем может быть проблема? (хостинг VDS Timeweb, то же самое на локалке)
Пожалуйста.
спасибо. перерыл много чего, но ответ нашел только здесь.
Многие жалуются на то, что Revo тяжелый, тормознутый и т.п. Я хочу наглядно показать, насколько может разница нагрузка на сервер сайтом, разработанным правильно, и «не очень».
Вот до 9-го числа — это сайт разработанный теми, кто «научился вставлять в шаблоны санки и сниппеты MODX-тегами и устанавливать в систему кучу сторонних пакетов». Как это оскорбительно не звучало бы, но большинство MODX-разработчиков я бы охарактеризовал именно так.
9-ое — выполнял техническую оптимизацию после 16-ти часов, потому нагрузка еще зафиксированна.
После 9-го — стабильная нагрузка на сервер в пределах 1/5 общего лимита (именно процессор. На БД нагрузки и не было особо).
До этого пики несколько раз превышали порог с желтой зоной (за пределами дозволенного), и клиенту грозили прекратить оказывать услуги.
Так вот, я еще раз советую тем, у кого проблемы с производительностью: изучайте php и API MODX, меньше используйте MODX-теги, фильтры вывода и т.п. MODX — мощная платформа, но насколько удобен парсер MODX-а, настолько он не идеален в плане производительности.
UPD: Данный сайт крутится на timeweb.ru за 120 руб. в месяц.
Наверняка многие, кто достаточно хорошо знает MODX, и уже создавал свои расширенные классы на основе стандартных классов MODX-а, оценил, насколько MODX гибкая и удобная платформа для разработок (сразу оговорюсь, что я не буду здесь рассказывать в подробностях, как создавать свои классы, расширяющие базовые, не мало топиков посвящено теме расширения базовых объектов). Здесь вам и пользовательские документы (ресурсы, CRC), и контроллеры да процессоры свои. Да, это все классно! Но пробовал кто-нибудь создавать свои, расширенные классы пользователей (типа modMyUser extends modUser)?
В чем задача? Допустим, я хочу расширить базовый класс modUser, и так, чтобы в самом объекте $modx->user был именно мой расширенный класс, а не базовый modUser. Зачем? Хотя бы для того, чтобы я мог в любом месте выполнить свою пользовательскую функцию на объекте $modx->user, к примеру $modx->user->getKarma(), и получить карму пользователя. Но плодить таблицы для своих пользователей, а главное самих пользователей (что мы наблюдаем в том же Discuss), не хочется вообще. То есть хочется, чтобы основные данные содержались в таблице modx_users.
Итак, что нам дает сам MODX для реализации желаемого?
Если посмотреть структуру таблицы modx_users, то можно увидеть там колонку class_key, и по умолчанию все записи в ней содержат значение modUser. Что это дает? Это позволяет указать собственный класс для отдельных пользователей. К примеру, я создаю вот такой класс:
<?php require_once MODX_CORE_PATH.'model/modx/moduser.class.php'; class myUser extends modUser { function test(){ return "Some text"; } } ?>
И можно выполнить вот такой код:
$user = $modx->getObject('modUser', $id); print $user->test();
Или даже просто $modx->user->test(); (если пользователь авторизован). Выглядит интересно, не правда ли? Но есть минусы, о которых скажу чуть позже, а пока выясним почему же при вызове объекта 'modUser', мы получаем объект 'myUser'. Для этого посмотрим на метод xPDOObject::load()
public static function load(xPDO & $xpdo, $className, $criteria, $cacheFlag= true) { $instance= null; $fromCache= false; if ($className= $xpdo->loadClass($className)) { if (!is_object($criteria)) { $criteria= $xpdo->getCriteria($className, $criteria, $cacheFlag); } if (is_object($criteria)) { $row= null; if ($xpdo->_cacheEnabled && $criteria->cacheFlag && $cacheFlag) { $row= $xpdo->fromCache($criteria, $className); } if ($row === null || !is_array($row)) { if ($rows= xPDOObject :: _loadRows($xpdo, $className, $criteria)) { $row= $rows->fetch(PDO::FETCH_ASSOC); $rows->closeCursor(); } } else { $fromCache= true; } if (!is_array($row)) { if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Fetched empty result set from statement: " . print_r($criteria->sql, true) . " with bindings: " . print_r($criteria->bindings, true)); } else { $instance= xPDOObject :: _loadInstance($xpdo, $className, $criteria, $row); if (is_object($instance)) { if (!$fromCache && $cacheFlag && $xpdo->_cacheEnabled) { $xpdo->toCache($criteria, $instance, $cacheFlag); if ($xpdo->getOption(xPDO::OPT_CACHE_DB_OBJECTS_BY_PK) && ($cacheKey= $instance->getPrimaryKey()) && !$instance->isLazy()) { $pkCriteria = $xpdo->newQuery($className, $cacheKey, $cacheFlag); $xpdo->toCache($pkCriteria, $instance, $cacheFlag); } } if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Loaded object instance: " . print_r($instance->toArray('', true), true)); } } } else { $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'No valid statement could be found in or generated from the given criteria.'); } } else { $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Invalid class specified: ' . $className); } return $instance; }
И здесь есть вот такая строка: $instance= xPDOObject :: _loadInstance($xpdo, $className, $criteria, $row); Именно этот метод инициализирует конечный объект (и наполняет его данными, полученными из базы данных (переменная $row)).
Посмотрим на этот метод внимательней.
public static function _loadInstance(& $xpdo, $className, $criteria, $row) { $rowPrefix= ''; if (is_object($criteria) && $criteria instanceof xPDOQuery) { $alias = $criteria->getAlias(); $actualClass = $criteria->getClass(); } elseif (is_string($criteria) && !empty($criteria)) { $alias = $criteria; $actualClass = $className; } else { $alias = $className; $actualClass= $className; } if (isset ($row["{$alias}_class_key"])) { $actualClass= $row["{$alias}_class_key"]; $rowPrefix= $alias . '_'; } elseif (isset($row["{$className}_class_key"])) { $actualClass= $row["{$className}_class_key"]; $rowPrefix= $className . '_'; } elseif (isset ($row['class_key'])) { $actualClass= $row['class_key']; } $instance= $xpdo->newObject($actualClass); if (is_object($instance) && $instance instanceof xPDOObject) { $pk = $xpdo->getPK($actualClass); if ($pk) { if (is_array($pk)) $pk = reset($pk); if (isset($row["{$alias}_{$pk}"])) { $rowPrefix= $alias . '_'; } elseif ($actualClass !== $className && $actualClass !== $alias && isset($row["{$actualClass}_{$pk}"])) { $rowPrefix= $actualClass . '_'; } elseif ($className !== $alias && isset($row["{$className}_{$pk}"])) { $rowPrefix= $className . '_'; } } elseif (strpos(strtolower(key($row)), strtolower($alias . '_')) === 0) { $rowPrefix= $alias . '_'; } elseif (strpos(strtolower(key($row)), strtolower($className . '_')) === 0) { $rowPrefix= $className . '_'; } $parentClass = $className; $isSubPackage = strpos($className,'.'); if ($isSubPackage !== false) { $parentClass = substr($className,$isSubPackage+1); } if (!$instance instanceof $parentClass) { $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Instantiated a derived class {$actualClass} that is not a subclass of the requested class {$className}"); } $instance->_lazy= $actualClass !== $className ? array_keys($xpdo->getFieldMeta($actualClass)) : array_keys($instance->_fieldMeta); $instance->fromArray($row, $rowPrefix, true, true); $instance->_dirty= array (); $instance->_new= false; } return $instance; }
Здесь самое интересное вот это:
if (isset ($row["{$alias}_class_key"])) { $actualClass= $row["{$alias}_class_key"]; $rowPrefix= $alias . '_'; } elseif (isset($row["{$className}_class_key"])) { $actualClass= $row["{$className}_class_key"]; $rowPrefix= $className . '_'; } elseif (isset ($row['class_key'])) { $actualClass= $row['class_key']; } $instance= $xpdo->newObject($actualClass);
То есть здесь xPDO получает из записи название конечного класса, и создает новый объект (в дальнейшем его наполняя данными $instance->fromArray($row, $rowPrefix, true, true);). Вот этот объект мы и получим на выходе. Собственно этот же механизм реализован и в документах, там тоже в одной таблице данные различных типов объектов с указанием класса в колонке class_key (таблица modx_site_content).
Вот, собственно уже неплохо — свои объекты можно создавать. А чего мне не хватает? А не хватает возможности указывать MODX-у названия текущего класса пользователей. Объясню. Давайте взглянем на метод modX::getAuthenticatedUser()
public function getAuthenticatedUser($contextKey= '') { $user= null; if ($contextKey == '') { if ($this->context !== null) { $contextKey= $this->context->get('key'); } } if ($contextKey && isset ($_SESSION['modx.user.contextTokens'][$contextKey])) { $user= $this->getObject('modUser', intval($_SESSION['modx.user.contextTokens'][$contextKey]), true); if ($user) { $user->getSessionContexts(); } } return $user; }
То есть хотим мы этого, или нет, но первоначально будет выполнена именно инициализация объекта modUser. Это дальше уже будет выполнен метод modUser::load(), и будет подгружен итоговый класс, указанный в колонке class_key (сам по себе класс modUser не имеет метода load(), но он его наследует от родительского класса). А мне хотелось бы иметь возможность указывать какой именно класс использовать по умолчанию, чтобы, к примеру, на уровне метода __construct() выполнить какой-то особый код. Зачем это может понадобиться? Это бы позволило для разных контекстов для одних и тех же пользователей (именно их данных) создавать разные объекты. К примеру, заходя на один домен, у нас обычный пользователь modUser, а на другой домен (где, к примеру, форум), создается другой пользователь, со своим, уникальным функционалом. К примеру, у вас большой разветвленный проект, с большой базой пользователей. Но, к примеру, один контекст — это внешняя социалка, а другой контекст — это закрытый резурс. Если бы можно было для конкретного контекста указать тип пользователя, то объект из одного контекста просто не обладал бы функционалом объектов из другого контекста.
Конечно, все это слишком мудренно, и многим скорее всего покажется абсолютно не нужным, но, дело всего в двух строчках кода в ядре, а прирост в гибкости все-таки существенный. Вот мне это сейчас надо.
Напоследок небольшой пример, как можно было бы тогда рулить, какие объекты в итоге возвращать в $modx->user.
class SocietyUser extends modUser { static function load(xPDO &$xpdo, $className, $criteria, $cacheFlag = true) { $instance= null; $fromCache= false; if ($className= $xpdo->loadClass($className)) { if (!is_object($criteria)) { $criteria= $xpdo->getCriteria($className, $criteria, $cacheFlag); } if (is_object($criteria)) { $row= null; if ($xpdo->_cacheEnabled && $criteria->cacheFlag && $cacheFlag) { $row= $xpdo->fromCache($criteria, $className); } if ($row === null || !is_array($row)) { if ($rows= xPDOObject :: _loadRows($xpdo, $className, $criteria)) { $row= $rows->fetch(PDO::FETCH_ASSOC); $rows->closeCursor(); } } else { $fromCache= true; } if (!is_array($row)) { if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Fetched empty result set from statement: " . print_r($criteria->sql, true) . " with bindings: " . print_r($criteria->bindings, true)); } else { // Перегружаем значение класса $row['SocietyUser_class_key'] = 'SocietyTestUser'; $instance= xPDOObject :: _loadInstance($xpdo, $className, $criteria, $row); if (is_object($instance)) { if (!$fromCache && $cacheFlag && $xpdo->_cacheEnabled) { $xpdo->toCache($criteria, $instance, $cacheFlag); if ($xpdo->getOption(xPDO::OPT_CACHE_DB_OBJECTS_BY_PK) && ($cacheKey= $instance->getPrimaryKey()) && !$instance->isLazy()) { $pkCriteria = $xpdo->newQuery($className, $cacheKey, $cacheFlag); $xpdo->toCache($pkCriteria, $instance, $cacheFlag); } } if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Loaded object instance: " . print_r($instance->toArray('', true), true)); } } } else { $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'No valid statement could be found in or generated from the given criteria.'); } } else { $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Invalid class specified: ' . $className); } return $instance; } function test(){ return "11Sdfsdfsd"; } }
В продолжение радостной новости о выходе новой версии Discuss, публикую обзор.
Сначала о грустном…
В процессе установки вылезли SQL-ошибки.
Error adding index user to disUserFriend: Array ( [0] => 42S02 [1] => 1146 [2] => Table 'discuss_v01.modx_discuss_user_friends' doesn't exist ) Error adding index friend to disUserFriend: Array ( [0] => 42S02 [1] => 1146 [2] => Table 'discuss_v01.modx_discuss_user_friends' doesn't exist ) Error adding field disBoard->ltr: No metadata defined Error adding index ltr to disBoard: No metadata defined Error adding field disThread->post_last_on: Array ( [0] => 42S02 [1] => 1146 [2] => Table 'discuss_v01.modx_discuss_threads' doesn't exist ) Error adding index post_last_on to disThread: Array ( [0] => 42S02 [1] => 1146 [2] => Table 'discuss_v01.modx_discuss_threads' doesn't exist ) Error adding field disThread->participants: Array ( [0] => 42S02 [1] => 1146 [2] => Table 'discuss_v01.modx_discuss_threads' doesn't exist ) Could not load class: disBanGroup from mysql.disbangroup. Could not load class: disBanItem from mysql.disbanitem.
Запустил переустановку пакета (из расчета того, что таблицы уже есть и все дело в очередности вызова скриптов). Получил новую порцию ошибок:
Error adding index user to disUserFriend: Array ( [0] => 42000 [1] => 1061 [2] => Duplicate key name 'user' ) Error adding index friend to disUserFriend: Array ( [0] => 42000 [1] => 1061 [2] => Duplicate key name 'friend' ) Error adding field disBoard->ltr: No metadata defined Error adding index ltr to disBoard: No metadata defined Error adding field disThread->post_last_on: Array ( [0] => 42S21 [1] => 1060 [2] => Duplicate column name 'post_last_on' ) Error adding index post_last_on to disThread: Array ( [0] => 42000 [1] => 1061 [2] => Duplicate key name 'post_last_on' ) Error adding field disThread->participants: Array ( [0] => 42S21 [1] => 1060 [2] => Duplicate column name 'participants' ) Could not load class: disBanGroup from mysql.disbangroup. Could not load class: disBanItem from mysql.disbanitem.
С индексами еще ладно, просто ошибка попытки создать уже существующий индекс, а вот за невозможностью подгрузки класса «disBanItem» надо еще будет последить.
Javascript-ошибки действительно есть, типа этой: «ReferenceError: $LAB is not defined». И видим вот такие неприятные моменты: ?
Но это мелочи… Самое главное, что бросается в глаза, и хотя еще не изучены последствия, но уже очень грустно становится — это отдельная таблица для пользователей форума, при чем со своими полями username, email и т.п. При чем изменение данных у основного пользователя не влияет на данные пользователя форума, даже если это емейл, или пользователя вообще заблокировали. Это очень и очень грустно…
При попытке входа не выводится никаких ошибок о неправильном логине и пароле (на официальном форуме перебрасывает на самостоятельную страницу авторизации на сам modx.com).
Еще один большой минус — для работы форума требуется куча правил реврайта. Тоже вообще не порадовало. Кстати, хотя он по умолчанию заточен под адрес /forums/, и создает для себя документ при установке (и, к слову, при переустановке тоже, каждый раз), тем не менее его пути все равно формируются от корня. Это происходит по той причине, что при работе он получает настройку discuss.forums_resource_id и на основе ID этого документа формирует ссылку к корню. Но при этом в момент установки он не ставит id созданного собой же документа в эту переменную. Так что не забывайте вручную установить корневой документ.
Так же сразу видно, что основа закладывалась очень давно, и много просто переходит по наследству, но уже давно не работает. К примеру GET-параметр print должен сигнализировать о том, чтобы вывести версию для печати, и он действительно выводит урезанную версию страницы, да вот только файл print.css отсутствует. В итоге страничка получается вообще не пригодная для чтения.
Система шаблонизации оставляет желать лучшего. Это если мягко выражаться… Куча файлов tpl-ок с огромным количеством плейсхолдеров и т.п. А файл-манифест, описывающий правила для отдельных разделов содержит 500+строк кода и выглядит вот так:
$manifest = array( 'preview' => 'preview.png', 'global' => array( 'js' => array( 'inline' => 'var DIS = {config: {}}; DIS.url = "'.$this->discuss->request->makeUrl().'";DIS.shJsUrl = "'.$this->discuss->config['jsUrl'].'sh/";DIS.config.connector = "'.$this->discuss->config['connectorUrl'].'"; DIS.config.forum_url = "'.$this->discuss->request->makeUrl().'"', ), 'options' => array( 'registerJsToScriptTags' => false, 'showBreadcrumbs' => true, 'showTitleInBreadcrumbs' => true, 'showReaders' => true, 'showModerators' => true, 'showPaginationIfOnePage' => false, 'showPrintOption' => false, ) ), 'print' => array( 'css' => array( 'header' => array( 'print.css', ), ), ),
Все через кучу контроллеров, свои классы-реквестеры и т.п. В общем довольно-таки жутко.
И самое интересное: когда создается пользователь в админке, он не создается в пользователях форума. А регистрацию я так и не проверил, так как для этого еще надо документ создавать (как и для обновления профиля, и для регистрации).
Есть еще куча всяких ошибок, но расписывать их лень. Мой вердикт: все это надо переписывать с нуля. То, как оно сейчас выглядит, развивать перспективы нет.
В общем я сильно разочарован… Реально. Livestreet для MODX (modLivestreet) — это конечно костыль, но зато устанавливается в пару кликов и сразу работает. А тут… Печально:-(
P.S. Плагин, чтобы работало без дополнительных правил в .htaccess и т.п. (вешаем его на событие OnPageNotFound) Может что-то доработать надо, но это можно узнать только в процессе работы.
<?php if($modx->context->key == 'mgr'){return;} if(isset($modx->resource) && is_object($modx->resource)){return;} $base_url = $modx->getOption('base_url', null); $url = $_SERVER['REQUEST_URI']; $forums_resource_id = $modx->getOption('discuss.forums_resource_id', null, $modx->getOption('site_start', null)); $forum_url_prefix = $modx->makeUrl($forums_resource_id, '', '', 'abs'); if(!$forum_url_prefix) $forum_url_prefix = $modx->getOption('base_url', null); // Check for forums path if(strpos($url, $forum_url_prefix) !== 0){return;} $preg = str_replace('/', '\/', $forum_url_prefix); $url = preg_replace("/^{$preg}/", $base_url, $url); $_SERVER['REQUEST_URI'] = $url; // cleare from params $url = preg_replace('/\?.*/','', $url); if(!$url){return;} $params = array(); if(preg_match('/^\/(board|thread|category)\/([0-9]+)\/(.*)$/', $url, $match)){ $params['action'] = $match[1]; $params[$match[1]] = $match[2]; } else if(preg_match('/^\/u\/(.*)$/', $url, $match)){ $params['action'] = 'user'; $params['user'] = $match[1]; } else{ $params['action'] = $url; } foreach($params as $k => $v){ $_GET[$k] = $v; $_REQUEST[$k] = $v; } $modx->sendForward($forums_resource_id);
P.P.S. До хорошего так и не добрался…
Продолжая тему создания компонентов для MODX Revolution и собственных репозиториев для них, хочу продемонстрировать, что можно сделать с помощью этого. И здесь не только дело в удобствах. В первую очередь — это демонстрация вашим же клиентам качественного продукта и высокого сервиса. А для вас, как для разработчика, это удобство по сопровождению проектов и гарантия автономности разрабатываемых компонентов.
Итак, смотрим видео.
Наткнулся сегодня на эту статью.
Думаю, эта информация будет крайне интересна веб-студиям и всем тем, кто разрабатывает свои дополнения для MODX Revolution и задумывается о вопросах лицензирования.
Могу сказать, был очень рад прочитать о такой позиции MODX-разработчиков, что дает надежду на то, что разработав какое-то приложение для MODX, при желании его можно выпустить под коммерческой лицензией, а растущее количество пользователей MODX-сайта дает шансы на этом заработать.
Как MODX-разработчик, могу точно сказать, что можно написать очень много чего для MODX, при этом не затронув ни байта кода ядра. Это и дает иммунитет от «вирусности GPL».
Так что всем, кто еще не перешел полностью на MODX, советую всерьез об этом подумать. Я свой выбор сделал уже давно, и не жалею ни капли.
Этот пакет простой до безобразий, но крайне полезный в повседневной работе по обслуживанию сайтов. Часто бывает, что надо выполнить небольшой php-скрипт (чаще всего с API MODX), но создавать под это страничку, сниппет и т.п. — порой дольше, чем скрипт писать.
И вот лично я часто пользуюсь этим инструментом. Это мой самый первый пакет (написанный почти два года назад), и не все в нем автоматически прописывается (в частности не создается пункт меню, и его приходится вручную создавать), но готовится новая версия пакета, которая и устанавливаться полностью будет, и подсветка кода будет (Ace, CodeMirror), выполнение скрипта по нажатию Ctrl+R и т.п.
Но самая прикольная предполагаемая фишка — журнал выполняемых скриптов. Идея такова, чтобы если что-то уже писал и выполнял, можно было найти в журнале и подставить в текущий код. Так что устанавливайте и пробуйте текущую версию, а когда выйдет новая версия, вы узнаете об этом из админки своего сайта и накатите обновление. Репозиторий: rest.modxstore.ru/extras/
UPD: Исходник на гитхабе: github.com/Fi1osof/modx-console