Николай Ланец
2 мая 2013 г., 11:46

О пакетах, обратной совместимости, стандартах и процессорах на замену сниппетам

Тематика этого топика будет довольно обширная, но опишу все в одном топике, так как все связано. Постараюсь кратко.
Для начала о пакетах
Любой, кто хоть что-то пытался разработать на MODX-е, знаком с менеджером пакетов (ставил Wayfinder, getResource и т.п.). При этом далеко не все, кто уже что-то пишет под MODX самостоятельно (сниппеты, плагины и т.п.), оформляет свой код в пакеты. А вот это очень и очень зря. Во-первых, часто такой код плохо локализован, разбросан по сайту и мало с чем совместим, он просто есть на сайте и все, но его сложно отделить от сайта и перенести на другой сайт. А это уже и риски того, что при обновлении сайта что-то может сломаться, да еще и потеря собственных наработок. Так что, учитесь оформлять все свои наработки в пакеты. И это совершенно не сложно. Если у вас простой пакет, без каких-то выполняемых при установке скриптов (создания таблиц в базе данных и т.п.), то вы можете пакеты собираться packMan-ом. Там все просто и понятно. Упаковать можно шаблоны, сниппеты, чанки, плагины и другие пакеты, а так же любые директории сайта.
Пара рекомендаций. 1. Если что-то дорабатываете на своем сайте, всегда создавайте тематический неймспейс (namespace) и категорию. Чанки, сниппеты и т.п. обязательно по категориям распределяйте.
2. Скрипты и т.п. размещайте в папки в соответствии со стандартами (core/components/[namespace], assets/components/[namespace], manager/components/[namespace]).
3. Этот процесс может вам значительно упростить CMPgenerator. Вообще его прямое предназначение — генерировать модели и объекты из таблиц базы данных (и вы конечно же можете использовать его по назначению), но еще он умеет создавать правильно папки компонентов (останется только свои файлы по этим папкам раскидать).
Об обратной совместимости
Давайте рассмотрим простую ситуацию: вы написали для себя простой и полезный пакетик myPack-1.0.0-beta, и вы его установили на 5 своих сайтов. Все замечательно работает. А потом раз, и появилась необходимость этот компонент доработать (функционал чуть-чуть переделать). Вы делаете это на одном из этих сайтов, дорабатываете пакет и выпускаете новую версию myPack-2.0.0-beta. Затем вы этот пакет устанавливаете но другом сайте поверх пакета 1.0.0, и он перестает работать, ошибки лезут…
Да, такое бывает часто. Основные причины:
1. Использование в компоненте функционала сторонних пакетов (не ядра системы). Здесь все просто. Представьте, что в своем компоненте вы использовали выборку документов через getResources. А есть ли гарантия, что на любом сайте, куда будет устанавливаться пакет, будет установлен getResources? Само собой нет. И это надо учитывать при разработке компонента.
2. Модификация кода компонента после установки. Очень распространенная ошибка. Часто, после установки каких-то компонентов, программисту что-то да не хватает в них. Он берет, и подправляет пару строчек. А потом устанавливается обновление пакета, и все затирается… И эти пара строк теряются. Вот эта проблема чаще всего висит на совести разработчика компонента. Надо компонент разрабатывать так, чтобы можно было переопределять параметры, системные настройки и т.п. Особенно когда есть чанки для оформления вывода, надо обеспечивать возможность переопределения этих чанков (в сниппетах использовать переменные $tpl и т.п.).
3. Смена имен переменных, методов и т.п. Вот у вас был сниппет, который принимал параметр $tpl. Вы решили, что это не достаточно гибко, надо разбить на $outerTpl и $innerTpl. Сказано — сделано. У нас есть новые две переменные, и убили старую переменную. Но там, где этот пакет уже использовался, сниппет вызывался с параметром $tpl. Как вы думаете, каков будет результат после установки новой версии пакета? Результат вполне предсказуемый.
Вот эта третья причина как раз и имеет самое непосредственное отношение к обратной совместимости. Когда вы разрабатываете новую версию компонента, нельзя забывать про то API, что имелось в предыдущих версиях пакета. К примеру, тот же MODX Revolution очень длительное время поддерживал API MODX Evolution (теги не берем во внимание).
К слову, я вчера на одном сайте обновлял сразу три своих пакета phpTemplates, modxSmarty и shopModx. Версии были установлены уже довольно старые. После обновления всех трех пакетов, сайт продолжил работать как ни в чем не бывало. Просто появился новый функционал, который позволил раз в 10 сократить объем кода компонентов самого сайта, использующих эти пакеты (я переписал функционал сайта). Вот к этому как раз и надо стремиться — желая улучшить свой компонент, не убивайте жестко имеющийся в нем функционал. А если убиваете, делайте это постепенно, с предупреждениями.
О стандартах и процессорах на замену сниппетам
Внимание! Если вы еще не сильны в ООП, дальнейший материал будет крайне сложным, так что читать можно скорее всего только из любопытства, но польза его для вас будет крайне сомнительна, разве что как пища для размышлений
В последнее время я очень активно использую процессоры вместо сниппетов, и несколько раз писал об этом. В частности и в shopModx были включены особые процессоры для выполнения различных выборок. И некоторые люди выражали великое сомнение на этот счет, мол, процессоры — это совсем для другого, и нет в них необходимости и т.д. и т.п. А вот я скажу, что процессоры по нескольким параметрам значительно выигрывают по сравнению со сниппетами. Но отдельно выделю только два момента.
Момент первый. Расширяемость.
Вот этого нет в сниппетах, и не уверен, что будет (хотя не буду утверждать обратного). Каждый сниппет — это отдельная функция, а как известно в PHP нельзя переопределять функции, расширять их и т.п. Можно только написать новую функцию. А процессоры — это классы (старые не «классные» процессоры во внимание не беру). Вот классы уже можно расширять, переопределять и т.п. Это дает очень серьезную гибкость в разработке, а так же значительно упрощает сопровождение кода. Приведу очень большой, но довольно наглядный пример, состоящий из четырех расширяющих друг друга процессоров.
Процессор первый. Получает товары.
<?php 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-шками (довольно подробно об этом писал здесь). Так что же я делаю в своем пользовательском процессоре, расширяющем базовый? 1. Добавляю условие поиска только опубликованных документов товаров, не удаленных и не скрытых в меню.
$query->where(array( 'deleted' => 0, 'hidemenu' => 0, 'published' => 1, ));
2. Добавляю условие поиска только тех товаров, у которых есть картинки.
$query->innerJoin('modTemplateVarResource', 'image', "image.contentid = {$this->classKey}.id and image.tmplvarid=8 and image.value != ''");
3. В конечный вывод сразу добавляю переменную полного пути до картинки.
$d['image'] = $imagesUrl.$d['tvs']['sm_image']['value'];
Все. Теперь этот процессор будет мне возвращать данные о товарах только соответствующим этим параметрам. К примеру я могу вызывать процессор так:
<?php if($response = $modx->runProcessor('web/catalog/products/getdata', array( where => array( 'parent:in' => array(177,178,179), ), 'limit' => 2, ), array( 'processors_path' => $path, ))){ $products = $response->getObject(); }
В данном случае процессор вернет только два товара, имеющихся в моделях-разделах с ID 177,178,179. Поменяю limit — вернет другое количество. Поменяю условие where — само собой будет поиск с другими условиями.
Но на этом сайте есть особенность: товары — это как бы различные вариации моделей. То есть есть модель такая-то, а у нее может быть неопределенное количество вариаций (разные цвета, исполнения и т.п.). Так вот, чаще всего надо делать не просто выборку товаров, а именно по одной вариации на одну модель (сделал поиск товаров по условиям, но в выборку попадает только одна вариация для одной модели). Для этого есть расширяющий процессор, получающий модели товаров.
<?php /* * Получаем данные только моделей товаров */ require_once dirname(__FILE__).'/getdata.class.php'; 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 /* * Делаем выборку топовых товаров */ require_once dirname(__FILE__).'/getdata.class.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 строк в себе может иметь очень и очень большой функционал. А главное — стандартизированный, который к тому же и обратную совместимость обеспечивает.

Добавить комментарий