Да, это я в примере не прописал путь к пакету. Вместо $modx->addPackage('Geoip'); надо $modx->addPackage('Geoip', $modx->getOption('core_path').'components/geoip/model/');
скачал, установил... [2013-05-07 13:26:37] (ERROR @ /index.php) Invalid path specified for package: Geoip; using default xpdo model path: /paas/c0100/www/core/xpdo/om/ [2013-05-07 13:26:37] (ERROR @ /index.php) Could not load class: Geoip from mysql.geoip. что за беда?
Наверняка много кто использовал на своих сайтах пакет phpThumbOf — отличная штука, чтобы сделать превьюшки. Простой пример в использовании: [[phpthumbof?input=[[+source]]
&options=w=350&h=350&zc=1
]] На лету создаст превьюшку 350 на 350. Но с этим пакетом не все так просто. Проблема первая. Чтение кеша Важно понимать, что это не самостоятельный пакет. Он использует имеющийся в MODX класс phpthumb. А тот в свою очередь уже был ранее замечен, как источник проблем с производительностью. Дело в том, что phpthumb перед созданием кеша файла, подсчитывает общий объем кеша, и это процедура совсем не слабая, если файлов довольно много. Чтобы этого избежать, идем в настройки MODX-а и устанавливаем значение 0 для следующих настроек: phpthumb_cache_maxage, phpthumb_cache_maxfiles и phpthumb_cache_maxsize. Тогда phpthumb не будет подсчитывать объем использованного кеша. Проблема вторая. Низкая производительность. Сейчас делаю каталог недвижимости, и там на одну страницу выводится 30 карточек. Так вот, на генерацию 30-ти превьюшек уходит 3-4 секунды. Конечно в дальнейшем эти превьюшки уже будут, и повторной генерации не будет, но от этого все равно не легче, так как на сайте более 4000 активных объектов, и будет очень много тормознутых страниц. Да и сниппету каждый раз проверять наличие файла превьюшки — тоже лишняя работа. В общем, эту проблему я решил исправить радикально — просто заранее сгенерировать превьюшки и сохранить их для документа в отдельное TV-поле. Итак, все по порядку: 1. Создаем TV-шку для превьюхи. С этим все просто и понятно. Единственное — если на сайте будут работать менеджеры, то настройте права доступа на нее, чтобы они даже не видели это поле (это прям в редакторе TV-шки есть вкладка Права доступа). 2. Пишем плагин на сохранение документа. Замысел прост — при сохранении документа, плагин должен проверять значение TV-шки с необрезанной фоткой, и если получает, то выполняет сниппет phpThumbOf и записывает полученный путь до превьюшки в эту специальную TV-шку. Код плагина: Все. Теперь при сохранении документа сразу будет генериться и сохраняться превьюшка. Но что делать, если уже документы есть, и их много? Ведь надо каждый документ пересохранить… Но с этим тоже все довольно просто — не обязательно каждый документ открывать, можно документ пересохранить программно, через API. Лучше всего это делать через Console. Пишем и выполняем вот такой код: Здесь мы получаем документы с картинками, у которых есть картинки, но нет еще превьюшек, и обновляем их через процессор. А в процессоре уже вызывается событие OnDocFormSave и само собой и наш процессор, срабатывающий на это событие. Вот теперь, после того, как у нас уже есть все превьюшки в специальном TV, нам не надо уже в коде страницы вызывать сниппет phpThumbOf, мы просто вставляем значение этой TV-шки. Нагрузка падает, мы радуемся :-) UPD: Чуть-чуть дописал плагин. Добавил смену контекста, если текущий контекст — mgr. Дело в том, что для разных контекстов могут быть указаны различные источники файлов, а для mgr этот параметр в TV почему-то не задается, и если источник файлов TV-параметра не базовый (корневой), а контекст mgr, то TV-шка просто вернет неверный результат. UPD: Добавил еще проверку AND $photo != $thumb. Причина — pThumb, если не смог получить картинку или еще что (ошибка в общем) возвращает оригинальное изображение. Если мы запишем оригинальный путь, то у нас и TV-поле будет заполнено (и мы не будем знать, что для этого ресурса не была создана превьюшка), и значение будет не правильным. Будет потом много гемора.
Справедливости ради стоит отметить, что этот баг пофиксен месяца два назад. Вот последнее упоминание этой причины. Так что версии старше 2.0.0 можно удалять без особых.
Как-то ставил я minishop2, чтобы посмотреть что и как. А сегодня решил удалить его (не нужен). Удаление прошло довольно успешно (не считая кучи ошибок в логах), но что интересно — папка assets/images/ с кучей картинок самого сайта исчезла полностью. Не сложно было увязать между собой эти два события. Откатился назад (благо бекапы на modxcloud.com стабильно создаются), запустил деинсталлятор minishop-а опять и смотрю трассировку. А там картина маслом: ? То есть папка assets/images/ удаляется начисто. И вот зачем такое надо? Как показывают исходники, MS2 действительно для себя пытается создавать папку assets/images/, видимо думая, что эта папка никогда никому больше не может пригодиться, а стандарты assets/components/{package} вообще зря придумали. Вот интересно, как скоро багфикс появится, или это не считается багом? Спасибо автору расширения, что ядро не удаляет.
Опытным путем выяснил, что можно, к примеру, и вот такое творить: {assign var=cost value=array('500000 руб.', '3000000 руб.', '5000000 руб.')}
А ещё непонятно, какой тег надо указывать, иначе не отправляется сообщение? Вы уже настолько хорошо знаете HTML-теги, что любой тег воспринимаете как HTML :) Нет, здесь тег — это просто метка, то есть какое-то ключевое слово (или несколько). Можете писать что угодно, подходящее по смыслу. По вопросу: в том топике говорилось о том, как залить картинку в принципе, и как ее загрузить в содержимое страницы. Но в содержимое страницы (в поле Контент) можно залить сколько угодно картинок. А в каталогах как правило картинки выводятся из специальных полей (чаще всего это дополнительные TV-поля). Вот и у вас эти картинки так же в TV-полях. Просто такие поля часто разбиваются по категориям и не сразу их все видно. Вот ваш случай: ? Я думаю дальше вы уже разберетесь, там привычный файловый менеджер.
Раньше было поле «картинка к товару», через которое можно было закачать фото к товару. Тот пример, который дан в разделе для пользователей, показывает, как закачать фотку в текст документа, а не к товару. Не могу найти такое поле. Или теперь это делается совсем по-другому? Нужно сменить фото к товару Слингобусы Радуга (1011) на busi2_2. В ФАЙЛЫ его закачала. А ещё непонятно, какой тег надо указывать, иначе не отправляется сообщение?
Я думаю, много уже кто выполнял SQL-запросы через $modx->newQuery(), ->prepare(), ->execute(); Но далеко не все знают, что таким образом можно выполнять не только SELECT-запросы, но и UPDATE и DELETE. Для этого существуют методы xPDOQuery::command() и xPDOQuery::set(). Метод xPDOQuery::command() по умолчанию устанавливает тип запроса SELECT, но как видите, может и другие типы устанавливать. Приведу небольшой пример запроса: Конечный SQL-запрос этого скрипта: Как видите, у нас здесь и условия поиска, и установка новых значений есть. Само собой можно и более сложные запросы набросать. Второй вопрос — отладка таких запросов. Ведь при выполнении таких запросов при ошибках на уровне базы данных вы просто ничего не увидите. Здесь требуются дополнительные движения. В первую очередь надо учесть, что при выполнении метода $c->prepare() мы получаем объект PDOStatement (как результат выполнения метода xPDO::prepare()) Соответственно и работать надо именно с этим объектом и на уровне его методов. (собственно, это и происходит, когда мы выполняем $c->stmt->execute(), $c->stmt->fetchAll() и т.п.). И для вывода ошибок нам и нужно работать с этим объектом. К примеру, чтобы получить информацию о возникших SQL-ошибках, можно выполнить print_r($c->stmt->errorInfo());
Тематика этого топика будет довольно обширная, но опишу все в одном топике, так как все связано. Постараюсь кратко. Для начала о пакетах Любой, кто хоть что-то пытался разработать на MODX-е, знаком с менеджером пакетов (ставил Wayfinder, getResource и т.п.). При этом далеко не все, кто уже что-то пишет под MODX самостоятельно (сниппеты, плагины и т.п.), оформляет свой код в пакеты. А вот это очень и очень зря. Во-первых, часто такой код плохо локализован, разбросан по сайту и мало с чем совместим, он просто есть на сайте и все, но его сложно отделить от сайта и перенести на другой сайт. А это уже и риски того, что при обновлении сайта что-то может сломаться, да еще и потеря собственных наработок. Так что, учитесь оформлять все свои наработки в пакеты. И это совершенно не сложно. Если у вас простой пакет, без каких-то выполняемых при установке скриптов (создания таблиц в базе данных и т.п.), то вы можете пакеты собираться packMan-ом. Там все просто и понятно. Упаковать можно шаблоны, сниппеты, чанки, плагины и другие пакеты, а так же любые директории сайта. Пара рекомендаций.
if($this instanceof Modxsite){ $modxsite = & $this; } else{ $modxsite = & $this->modxsite; } $modxsite->loadProcessor('web.resourceproduct.getdata', 'shopmodx');
class modWebCatalogProductsGetdataProcessor extends modWebResourceproductGetDataProcessor{
public function initialize() {
if(!$this->getProperty('sort')){
$this->setProperty('sort', "{$this->classKey}.menuindex");
$this->setProperty('dir', "ASC");
}
return parent::initialize();
}
protected function prepareCountQuery(xPDOQuery &$query) {
$query = parent::prepareCountQuery($query);
$query->innerJoin('modTemplateVarResource', 'image',
"image.contentid = {$this->classKey}.id and image.tmplvarid=8 and image.value != ''");
$query->where(array(
'deleted' => 0,
'hidemenu' => 0,
'published' => 1,
));
return $query;
}
/*
* Подготавливаем данные товара
*/
public function iterate(array $data) {
$data = parent::iterate($data);
// УРЛ источника картинок:
$imagesUrl = $this->modx->runSnippet('getSourcePath');
foreach($data as & $d){
// Получаем картинку
$d['image'] = $imagesUrl.$d['tvs']['sm_image']['value'];
// Набиваем иконки
$icons = array();
if(!empty($d['tvs']['sm_new']['value'])){$icons[] = 'i-new';}
if(!empty($d['tvs']['sm_stock']['value'])){$icons[] = 'i-stock';}
if(!empty($d['tvs']['sm_discount']['value'])){$icons[] = 'i-discount';}
$d['icons'] = $icons;
}
return $data;
}
}
return 'modWebCatalogProductsGetdataProcessor'; Конечно этот процессор не самый корневой, так как он расширяет shopModx-овый modWebResourceproductGetDataProcessor, а тот в свою очередь расширяет еще несколько процессоров, но это не принципиально, так как для нас это процессор из другого компонента, а в рамках нашего сайта представленный процессор является корневым. Но для полной управляемости нам само собой необходимо это знать и изучить shopModx-процессоры отдельно. Итак, shopModx-овый процессор делает выборку документов товаров, но без каких-то дополнительных условий, то есть не учитывает опубликован документ или нет, удален или нет и т.п. Плюс к этому оформляет все в виде массива данных со всеми TV-шками (довольно подробно об этом писал здесь). Так что же я делаю в своем пользовательском процессоре, расширяющем базовый?
class modWebCatalogProductsGetmodeldataProcessor extends modWebCatalogProductsGetdataProcessor{ protected $modelsIDs = array();
public function initialize() {
if(!$this->getProperty('sort')){
$this->setProperty('sort', "Models.menuindex");
$this->setProperty('dir', "ASC");
}
return parent::initialize();
}
// Готовим запрос на выборку уникальных объектов
protected function PrepareUniqObjectsQuery(xPDOQuery &$query) {
// Группируем по родителям
$query->groupby("{$this->classKey}.parent");
return parent::PrepareUniqObjectsQuery($query);
}
/*
* Подсчитываем количество товаров именно по ID-шникам их предков (то есть именно моделей),
* так как нам нужны уникальные модели
*/
protected function countTotal($className, xPDOQuery &$criteria) {
$count= 0;
if ($query= $this->modx->newQuery($className, $criteria)) {
if (isset($query->query['columns'])) $query->query['columns'] = array();
$query->select(array ("COUNT(DISTINCT {$this->classKey}.parent)"));
if ($stmt= $query->prepare()) {
// print $query->toSQL();
if ($stmt->execute()) {
if ($results= $stmt->fetchAll(PDO::FETCH_COLUMN)) {
$count= reset($results);
$count= intval($count);
}
}
}
}
return $count;
}
public function prepareQueryBeforeCount(xPDOQuery $c){
$c = parent::prepareQueryBeforeCount($c);
$c->innerJoin('modResource', 'Models', "Models.id = {$this->classKey}.parent");
return $c;
}
public function setSelection(xPDOQuery $c) {
$c = parent::setSelection($c);
$c->select(array(
'Models.id as model_id',
'Models.pagetitle as model_title',
));
return $c;
}
} return 'modWebCatalogProductsGetmodeldataProcessor'; Здесь происходит все то же самое, что и в родительском процессоре (то есть те же проверки на опубликованность, наличие картинки и т.п.), но плюс к этому выполняется выборка именно уникальных товаров для уникальных моделей. Конечный массив данных почти не отличается от результата выполнения предыдущего процессора, только в массив данных товара попадут еще ID модели и заголовок модели. Далее у нас задача еще усложняется. Нам надо сделать выборку всех моделей товаров из целого раздела на два три вложенности. И вот еще один процессор: <?php /*
require_once dirname(dirname(FILE)).'/getmodeldata.class.php';
class modWebCatalogProductsBycatalogsectionGetdataProcessor extends modWebCatalogProductsGetmodeldataProcessor{ protected $modelsCount = 0; // Общее число моделей protected $productsCount = 0; // Общее число товаров
/*
* Проверяем наличие переменной ID категории
*/
public function initialize() {
if(!((int)$this->getProperty('categoryid', false))){
return 'Не был указан ID категории';
}
$this->setDefaultProperties(array(
'limit' => 10,
));
return parent::initialize();
}
/*
* Делаем поиск моделей товаров, чтобы в дальнейшем искать конечные товары
*/
public function beforeQuery() {
// Собираем ID-шники моделей только в одной категории
$c = $this->modx->newQuery('modResource');
$c->innerJoin('modResource', 'Parent');
$c->where(array(
'Parent.parent' => (int)$this->getProperty('categoryid'),
'Parent.deleted' => 0,
'Parent.hidemenu' => 0,
'Parent.published' => 1,
'deleted' => 0,
'hidemenu' => 0,
'published' => 1,
'class_key' => 'ShopmodxResourceProductModel',
));
$c->select(array(
"DISTINCT modResource.id"
));
if($c->prepare() AND $c->stmt->execute() AND $rows = $c->stmt->fetchAll(PDO::FETCH_ASSOC)){
foreach($rows as $row){
$this->modelsIDs[] = $row['id'];
}
}
else{
$this->modx->log(xPDO::LOG_LEVEL_ERROR, "Не удалось получить ID-шники моделей товаров");
$this->modx->log(xPDO::LOG_LEVEL_ERROR, print_r($c->stmt->errorInfo(), true));
}
/*
* categoryid
*/
return parent::beforeQuery();
}
protected function prepareCountQuery(xPDOQuery &$query) {
$query = parent::prepareCountQuery($query);
$query->select(array(
'parent'
));
$query->where(array(
'parent:IN' => $this->modelsIDs,
));
// Подсчитываем общее количество моделей и вариантов
$this->countModelsAndVariables($query);
return $query;
}
// Подсчитываем общее количество моделей и вариантов
protected function countModelsAndVariables(xPDOQuery $query){
$c = clone $query;
if (isset($c->query['columns'])) $c->query['columns'] = array();
$c->select(array(
"COUNT(DISTINCT {$this->classKey}.id) as productsCount",
"COUNT(DISTINCT {$this->classKey}.parent) as modelsCount",
));
if ($stmt= $c->prepare()) {
if ($stmt->execute()) {
if ($results= $stmt->fetchAll(PDO::FETCH_ASSOC)) {
if($result = current($results)){
$this->modelsCount = $result['modelsCount'];
$this->productsCount = $result['productsCount'];
}
}
}
}
return;
}
public function prepareQueryAfterCount(xPDOQuery $c) {
$c = parent::prepareQueryAfterCount($c);
$c->innerJoin('modResource', 'vendor', "Models.parent=vendor.id");
return $c;
}
public function setSelection(xPDOQuery $c) {
$c = parent::setSelection($c);
$c->select(array(
'vendor.id as vendor_id',
'vendor.pagetitle as vendor_title',
));
return $c;
}
/*
* Готовим окончательный вывод
*/
public function outputArray(array $array, $count = false) {
$output = parent::outputArray($array, $count);
$output['modelsCount'] = $this->modelsCount; // Выводим общее кол-во моделей
$output['productsCount'] = $this->productsCount; // Выводим общее кол-во товаров
return $output;
}
} return 'modWebCatalogProductsBycatalogsectionGetdataProcessor'; И последняя задачка — выборка из всего раздела, но только ТОПовых товаров. Но это совсем маленький процессор:-) <?php /*
class modWebCatalogProductsBycatalogsectionGetdatatopProcessor extends modWebCatalogProductsBycatalogsectionGetdataProcessor{ protected function prepareCountQuery(xPDOQuery &$query) { $query = parent::prepareCountQuery($query); $query->innerJoin('modTemplateVarResource', 'top', "top.contentid = {$this->classKey}.id and top.tmplvarid=4 and top.value != ''"); return $query; } } return 'modWebCatalogProductsBycatalogsectionGetdatatopProcessor'; Да, это совсем не мало кода. Но если все это писать на сниппетах, то получится еще больше, и на много. Но главное — если, скажем, мне надо будет выводить уже все товары, без учетов наличия в них картинок, я изменю только один (базовый) процессор, и логика поменяется во всех дочерних процессорах. Или если мне надо будет добавить какие-то новые данные в объект товара, то мне тоже достаточно будет сделать это в одном единственном процессоре, а не переписывать каждый в отдельности. Плюс эти процессоры можно вызвать вообще из любого положения, хоть в плагине, хоть в сниппете, хоть в Смарти-шаблоне. Можно вообще на лету получить класс процессора и расширить его, не создавая для этого отдельного файла. Момент второй. Стандарты. Вот это тоже очень важный момент. Сниппеты не имеют никаких стандартов. Там пишется произвольный PHP-код и все (хотя многие туда еще HTML пишет, а еще бывает javascript, SQL и т.п.)) ). Так вот, процессоры — это классы со своими стандартами и со своим набором методов. И shopModx-овые процессоры поддерживают эти стандарты. Так что если вы изучите и поймете базовые процессоры самого MODX-а (и особенно Object-процессоры), то вам будет многое понятно и в сторонних процессорах. Там есть уже прописанные методы проверки прав, подгрузки языковых топиков, обработчики ошибок и т.д. и т.п. Так что процессор на 10 строк в себе может иметь очень и очень большой функционал. А главное — стандартизированный, который к тому же и обратную совместимость обеспечивает.