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

Я эти три параметра уже изнасиловал, но так ничего и не ускорилось. Насилую дальше.

Получилось еще сократить время загрузки главной страницы с одной секунды до 0.4-0.6 Оказывается, забыли про phpthumbof, который постоянно читает директорию кеша. В настройках выставил phpthumb_cache_maxage, phpthumb_cache_maxfiles и phpthumb_cache_maxsize в ноль, и стразу все зашуршало.

Я недавно выкладывал на ютуб ролик (смотрим под катом), в котором демонстрировал этот импортер, и вот кратко здесь про него напишу. Для начала задача: выполнить импорт 13 000 товаров (и не просто импорт, а еще и с проверкой существующих товаров, разделов и т.п., и обновлением остатков и цен, если товар уже есть, а еще и отметкой «нет в наличии» тех товаров, которые уже есть в каталоге, но которых нет в файле импорта), при чем так, чтобы уложиться в 30 секунд и 64 Mb памяти. В общем, я сразу решил, что в такие рамки не уложиться, и что надо написать такой модуль, который работал бы в цикле (пока задача не будет выполнена), при этом, чтобы вся логика полностью рулилась на стороне сервера. В общем, вот это как раз и есть такое решение (чуть подробней в ролике). Отмечу только, что это дело очень похоже на стандартный компонент MODx.Console, но это не он. Нативный компонент я попробовал, но отмел из-за того, что он только асинхронные запросы отправляет, не дожидаясь ответа. В общем, морду писал сам.

? Собственно говоря, сейчас еще можно увидеть hamster-fox.ru на чистом minishop + Revolution. Обязательно покликайте новинки, разделы и т.п. (отдельные страницы до 10 МИНУТ выполняются). Грустно все… Если у кого сомнения по поводу того, что это не вина минишопа и т.п., лучше просто оставьте при себе свое мнение:-) Модифицированный getResources — не лучший инструмент для многоуровневой выборки из 5000 документов с TV-шками и т.п. А вот это немного измененный магазин: hamster-fox-ru.fi1osof.modxcloud.com/ На самом деле почти ничего и не сделано, просто использованы процессоры shopModx и связка phpTemplates+modxSmarty. Каталог не из кеша, полностью на лету, даже еще и постраничность прикрутил (на старом сайте от нее отказались еще год назад, так как VDS просто умирает). А этот как вы видите, на modxcloud.com крутится. Обновил все сегодня за день. То есть сам магазин, корзина и т.п. — это все на minishop осталось, а каталог через shopModx работает. Но результатом все равно не доволен. 0,5-2 сек на страницу — много. Могу точно сказать, что система изначально не удачно разработана. UPD: Про корень зла и примеры кодов. Почему так тормозит минишоп? Основная причина в том, что в минишопе используется для выборки товаров модифицированный getResources. А всем известно, что данный компонент просто не создан для того. чтобы делать выборки из большого количества ресурсов, особенно когда много родителей и уровней вложенности больше, чем один. Он проходится рекурсивно по всем уровням и собирает ID-шники всех родительских документов. При чем для всех выборок использует getCollection(). Но в данном магазине это вообще жесть, так как разделов очень много (284, если быть точным). В итоге вот такой запрос складывается: gist.github.com/Fi1osof/c3aafeb797c20f370b73 (и это еще только часть запроса). А если попробовать перейти в раздел Новинки, к примеру, то еще и поиск по TV-полю включается, и в таком случае был зафиксирован рекорд — 10 минут выполнение на VDS (сейчас этот раздел вообще где-то в конце третьей минуты разваливается критической ошибкой нехватки ресурсов и времени на выполнение). Плюс мне вообще не понятно зачем все завязано на шаблонах? Если у нас есть жесткая связь с ModGoods (innerJoin), и эта связь — только для товаров, то зачем еще поиск по шаблону вести? Это утяжеляет запрос. В общем, для решения этой проблемы я и использовал модифицированный getData-процессор из shopModx. Вот конечный код: <?php

/*

  • Получаем данные каталога */

if($this instanceof Modxsite){ $modxsite = & $this; } else{ $modxsite = & $this->modxsite; } $modxsite->loadProcessor('web.getdata', 'shopmodx');

class modWebCatalogGetdataProcessor extends ShopmodxWebGetDataProcessor{ public $defaultSortField = 'good.id'; public $defaultSortDirection = 'DESC';

public function initialize() {
    $this->setDefaultProperties(array(
        'limit' => 12,
    ));
    if(!empty($_REQUEST['page']) AND $page = (int)$_REQUEST['page'] AND $page > 1 AND $this->getProperty('limit', 0)){
        $this->setProperty('start', ($page-1) * $this->getProperty('limit'));
    }
    return parent::initialize();
}

public function prepareQueryBeforeCount(xPDOQuery $c) {
    $c = parent::prepareQueryBeforeCount($c);
    $c->innerJoin('ModGoods', 'good', "good.gid={$this->classKey}.id");
    return $c;
}

protected function prepareCountQuery(xPDOQuery &$query) {
    $query = parent::prepareCountQuery($query);
    
    $type = $this->getProperty('type', 'all');
    if($type != 'all'){
        switch($type){
            // Новинки
            case 'novelty':
                $query->innerJoin('modTemplateVarResource', 'novelty', 
                        "novelty.contentid={$this->classKey}.id AND novelty.tmplvarid=11 AND novelty.value='1'");
                break;
            // Хиты продаж
            case 'top':
                $query->innerJoin('modTemplateVarResource', 'top', 
                        "top.contentid={$this->classKey}.id AND top.tmplvarid=6 AND top.value='1'");
                break;
            // Скоро в продаже
            case 'soon':
                $query->innerJoin('modTemplateVarResource', 'soon', 
                        "soon.contentid={$this->classKey}.id AND soon.tmplvarid=12 AND soon.value='1'");
                break;
            default:;
        }
    }
    
    $query->where(array(
        'published' => 1,
        'deleted'   => 0,
        'hidemenu'   => 0,
    ));
    
    return $query;
}

public function setSelection(xPDOQuery $c) {
    $c = parent::setSelection($c);
    $c->select(array(
        'good.*',
    ));
    return $c;
}

public function outputArray(array $array, $count = false) {
    $this->modx->setPlaceholder('total', $count);
    $this->modx->runSnippet('getPage@getPage', array(
        'limit' => $this->getProperty('limit'),
    ));
    return parent::outputArray($array, $count);
}

}

return 'modWebCatalogGetdataProcessor'; То есть здесь и выборка товаров, и сортировка, и постраничность, и условия поиска новинок, топов и т.п. Как видите, код совсем не большой. При чем в родительский процессор можно вообще не лезть. Просто знайте, что здесь будет массив данных товаров вместе со всеми TV-шками. При чем это через чистые PDO-запросы без всяких лишних пакетов и т.п. А вот расширяющий процессор, который делает выборки товаров только в категории и подкатегориях: <?php

/*

  • Получаем данные каталога */

require_once dirname(dirname(FILE)).'/getdata.class.php';

class modWebCatalogCategoryGetdataProcessor extends modWebCatalogGetdataProcessor{ protected $sectionsIDs = array(); // Разделы

public function beforeQuery() {
    $can = parent::beforeQuery();
    if($can !== true){
        return $can;
    }
    
    $this->getSectionsIDs($this->getSectionsCondition());
    if(!$this->sectionsIDs){
        return "Не были получены разделы";
    }
    return true;
}

protected function getSectionsCondition(){
    return array(
        'id' => $this->modx->resource->get('id'),
    );
}

// Получаем ID-шники разделов
protected function getSectionsIDs($where){
    if(!$where){
        return;
    }
    $query = $this->modx->newQuery('modResource');
    $query->select(array(
        "DISTINCT {$this->classKey}.id",     
    ));
    $query->where(array(
        'deleted'   => 0,
        'published' => 1,
        'isfolder'  => 1,
        'hidemenu'  => 0,
        'template'  => 2,
    ));
    $query->where($where);
    
    if($query->prepare() && $query->stmt->execute() && $rows = $query->stmt->fetchAll(PDO::FETCH_ASSOC)){
        $result = array();
        foreach($rows as $row){
            $result[] = $row['id'];
        }
        $this->sectionsIDs = array_unique(array_merge($this->sectionsIDs, $result));
        return $this->getSectionsIDs(array(
            "parent:IN" => $result,
        ));
    } 
    return;
}

public function prepareCountQuery(xPDOQuery &$query) {
    $query = parent::prepareCountQuery($query);
    $query->where(array(
        "{$this->classKey}.parent:IN" => $this->sectionsIDs,
    ));
    return $query;
}     

}

return 'modWebCatalogCategoryGetdataProcessor'; Далее результат набиваем сами, как хотим, хоть в чанки, хоть еще куда-нибудь. Я в смарти набиваю. Кстати, есть с чем сравнить. Вот чанк, который использовался раньше: <ins class="row show-grid">

<div class="r [[+tv.novice_good:gt=`0`:then=`novice`]] [[+tv.top_buyed:gt=`0`:then=`top_buyed`]] [[+remains:equalto=`0`:then=`[[+tv.expected_qty:gt=`0`:then=`expected_qty`]]`]]">
    <div class="label [[+tv.novice_good:gt=`0`:then=`novice`]] 

[[+tv.top_buyed:gt=0:then=top_buyed]] [[+remains:equalto=0:then=[[+tv.expected_qty:gt=0:then=expected_qty]]]]"></div>

    <div class="picture">
        <img src="[[!If? &subject=`[[+img]]` 

&operator=!empty &then=[[+img:phpthumbof=w=170]] &else=/assets/hamster/css/images/no_photo.png]]" /> </div>

    <div class="info">

    <a href="[[~[[+id]]]]" class="title">[[+pagetitle]]</a>

    <span class="sku">Арт. [[+article]]</span><br />

    <div class="buy"><span class="price">[[+price]] 

<span class="currency">[[+currency:default=Р]]</span></span> <a href='#' class="addToCartLink" data-gid="[[+id]]">[[+tv.expected_qty:gt=0:then=Заказать:else=В корзину]] </a>

</div>
    <div class="descr">[[+introtext]]</div>

    </div>

</div>

</ins> А вот он же, но на Smarty: <ins class="row show-grid"> {assign var=block_class value=""}

{if !empty($product.tvs.novice_good.value) && $product.tvs.novice_good.value == 1} {assign var=block_class value="{$block_class} novice"} {/if} {if !empty($product.tvs.top_buyed.value) && $product.tvs.top_buyed.value == 1} {assign var=block_class value="{$block_class} top_buyed"} {/if} {if !empty($product.tvs.expected_qty.value) && $product.tvs.expected_qty.value == 1} {assign var=block_class value="{$block_class} expected_qty"} {assign var=basket_label value="В корзину"} {else} {assign var=basket_label value="Заказать"} {/if}

<div class="r {$block_class}">
    <div class="label {$block_class}"></div>

    <div class="picture">
        <img src="{if !empty($product.img)}{snippet name="phpthumbof" 

params="input={$product.img}&options=w=170"}{else}/assets/hamster/css/images/no_photo.png{/if}" /> </div>

    <div class="info">

    <a href="{link id=$product.object_id}" class="title">{$product.pagetitle}</a>

    <span class="sku">Арт. {$product.article}</span><br />

    <div class="buy"><span class="price">{$product.price} <span class="currency">Р</span></span> 
        <a href='#' class="addToCartLink" data-gid="{$product.object_id}">{$basket_label}</a>
    </div>

    <div class="descr">{$product.introtext}</div>

    </div>

</div>

</ins> На самом деле почти тоже самое, но с той разницей, что в Смарти это скомпиллированный PHP-шаблон, с полной поддержкой PHP и выполнением всего в одном месте, а в чанке все это — куча MODX-тегов, которые будут парситься MODX-ом, инициироваться куча новых объектов и т.п. Могу точно сказать, что разница в производительности очень существенная. Вторая проблема — меню каталога Как я говорил выше, меню каталога очень большое — 284 раздела. И работало это традиционно на Wayfinder. Я удалял из шаблона вообще все, оставлял только один Wayfinder, результат — почти 3 секунды. И это вообще не удивительно. Меню я тоже перевел на процессор, и теперь меню формируется за 0,2-0,3 секунды, и то только потому что в цикле приходится все элементы меню набивать в Smarty-шаблончике. Можно конечно вообще шаблончики эти перенести в сам процессор, чтобы инклюдов не выполнялось, тогда вообще мгновенно будет формироваться меню, но это уже не стал пока заморачиваться, так как это выполняется только при первом заходе на страницу, а дальше это уже просто HTML документа. Еще плюс этого процессора в том, что он не выполняет запросов к БД каждый раз. После полной очистки кеша он один раз набивает все элементы в массив, и кеширует их. А далее он формирует конечное меню уже из этого массива без запросов к БД. Вот код процессора: <?php

class modWebSidebarMenuIndexProcessor extends modObjectGetListProcessor{

protected $IDs = array();

public function initialize() {
    $this->setDefaultProperties(array(
        'startId'   => $this->modx->getOption('shopmodx.catalog_id', null, 0),
        'depth'     => 3,
        'levelClass' => 'level',
        'outerTpl'  => 'inc/menu/catalog/outer.tpl',
        'rowTpl'    => 'inc/menu/catalog/row.tpl',
        'sortby'    => 'pagetitle',
        'sortdir'   => 'ASC',
    ));
    return parent::initialize();
}


public function process() {
    $output = '';
    // get current doc id
    if($pid = $this->modx->resource->parent){
        $this->IDs[] = $this->modx->resource->id;
        while($doc = $this->modx->getObject('modResource', $pid)){
            $this->IDs[] = $doc->id;
            $pid = $doc->parent;
        }
    }
    
    if(!$items = $this->getMenu()){
        return $this->failure('');
    }
    
    $output = $this->fetchMenu($items);
    return $this->success($output);
}

protected function fetchMenu(array $items, $level=0){
    $level++;
    $outer = '';
    $rows = '';
    $levelClass = $this->getProperty('levelClass');
    foreach($items as $item){
        $this->count++;
        $wraper  = '';
        $cls = array();
        if($levelClass){
            $cls[] = "{$levelClass}{$level}";
        }
        if(in_array($item['id'], $this->IDs)){
            $cls[] = 'active';
        } 
        $item['cls'] = $cls;
        
        if(!empty($item['childs'])){
            $wraper = $this->fetchMenu($item['childs'], $level);
        }
        
        $this->modx->smarty->assign('wraper', $wraper);
        $this->modx->smarty->assign('item', array(
            'link' => $item['uri'],
            'title' => $item['menutitle'] ? $item['menutitle'] : $item['pagetitle'],
            'cls' => implode(" ", $item['cls']),
        ));
        $rows .= $this->modx->smarty->fetch($this->getProperty('rowTpl'));
    }
    $this->modx->smarty->assign('wraper', $rows);
    $output = $this->modx->smarty->fetch($this->getProperty('outerTpl'));
    return $output;
}


public function getMenu(){
    $key = "{$modx->context->key}/catalog_menu";
    if(!$items = $this->modx->cacheManager->get($key)){
        $startId = $this->getProperty('startId', 0);
        $depth = $this->getProperty('depth', 1);

        if($items = $this->_getMenu($startId, $depth)){
            $this->modx->cacheManager->set($key, $items);
        }
    }
    return $items;
}
  


protected function _getMenu($id, $depth){
    $depth--;
    $items = array();
    $q = $this->modx->newQuery('modResource', array(
        'parent' => $id,
        'deleted' => 0,
        'published' => 1,
        'hidemenu' => 0,
        'template'  => 2,
    ));
    $q->select(array(
        'id', 'parent', 'uri', 'alias', 'pagetitle', 'menutitle',
    ));
    if($sortby = $this->getProperty('sortby')){
        $q->sortby($sortby, $this->getProperty('sortdir', 'ASC'));
    }
    if($q->prepare() && $q->stmt->execute()){
        while($row = $q->stmt->fetch(PDO::FETCH_ASSOC)){
            $row['childs'] = array();
            if($depth>0){
                $row['childs'] = $this->_getMenu($row['id'], $depth);
            }
            $items[$row['id']] = $row;
        }
    }
    return $items;
}

} return 'modWebSidebarMenuIndexProcessor'; Выполняю его в Smarty так: {processor ns=modxsite action="web/sidebar/menu/index" assign=menu} {$menu.message} Только надо учитывать, что этот массив не учитывает права доступов к документам, так что если у вас есть какие-то приватные разделы в каталоге, то он в чистом виде не годится, придется подправлять. Хотя если разделов не много, то само собой и WF достаточно. Заключение Вот, собственно, и вся оптимизация. Но здесь есть еще к чему стремиться, и самое главное — это надо сделать оптимизацию базы данных. Многие пытаются выполнить оптимизацию кода MODX-а, но забывают, что на уровне запросов единственное что можно и нужно оптимизировать — это база данных. На производительность сложных запросов очень сильно влияют первичные и вторичные ключи. Вот у нас здесь выборка из трех таблиц идет (документы, товары, TV-шки), и их надо между собой связать с настройкой вторичных ключей. Подробно об этом я писал здесь. UPD 2: подробный кейс: modxclub.ru/blog/modx-club-portfolio/152.html

При этом я абсолютно уверен, что не потребуется ни бухгалтер, ни юрист, ибо они-то как раз и будут наполнять этот билинг купив его. А вот это абсолютно ошибочное мнение. Биллинг, это не просто так «средство для наполнения», это программный комплекс с четко заданной логикой. И эта логика закладывается не просто так, а в соответствии с законодательством и нормами. У тебя на 1% расхождение будет, и компания в штрафах погрязнет, а может даже и до уголовной ответственности дойдет (если обвинят в уклонении от налогов в крупном размере). P.S. тему предлагаю закрыть, это все вода.

Это из того разряда, что это тема для создания мощного продукта (или его каркаса для начала) и извлечения из него в дальнейшем большой прибыли. Конечно, билинги можно найти и сторонние, но вот как-то не все они достаточно универсальны. Подкинул как пищу для ума и орешек для зубков. При этом я абсолютно уверен, что не потребуется ни бухгалтер, ни юрист, ибо они-то как раз и будут наполнять этот билинг купив его. Усекаешь тему? PS. Парадоксально, но билинг есть даже в панели управления хостингом (у меня серверы и коммерческая панель) и я даже когда-то принимал платежи через него эксперементируя с хостингами. Правда, копеешные платежи, но счет-фактуры банальные и счета — есть. Правда через всякого рода там robox. Что касается эквайринга, то тут надо тупо уточнить в банках как это делается. И если там не нужен гарантированный депозит в многотыщ зеленых, то вот прямой путь к приему платежей за что угодно. И всё в таком духе…

Лайкнул каждый пакет. Давайте поддержим Николая, ведь реально хорошие вещи делает.