В последнее время все реже и реже появляются сложные задачи, но вот сегодня я потратил довольно много времени на решение парочки таких (в сумме часов 17 просидел)… Сразу скажу, что я не буду особо расписывать решения (хотя коды покажу). Это все так, пища для размышлений.
Задача первая. Выборка по одному варианту товара на одну модель из множества, с приоритетом на наличие картинки
Есть N моделей товаров. У каждой модели есть N вариантов (конечных товаров с индивидуальными параметрами (цвет, размер, изображение и т.п.)). Так вот, в каталоге в общем списке выводятся не товары (чтобы не было 100500 дублей с мелкими отличиями), а модели. Но картинки указаны именно у вариантов, а не у моделей. То есть мы получаем произвольную выборку товаров, но выводим именно модели на основе данных этих товаров. И загвоздка самая была именно в картинках. Дело в том, что в рамках одной модели у каких-то товаров картинки есть, а у каких-то нет. А выбрать надо только одну модель, и лучше, если именно с картинкой.
У кого-то вопрос может возникнуть «а что сразу не назначить картинку модели?». В данном случае это совершенно не оправдано, так как в зависимости от параметров поиска, в модели будет изображение только от того товара, который попал по условиям поиска.
Вот собственно этот процессор:
<?php /* * Получаем данные только моделей товаров * То есть по товару находим модель, и для модели получаем только одну вариацию товара */ require_once dirname(__FILE__).'/getdata.class.php'; class modWebCatalogProductsGetmodeldataProcessor extends modWebCatalogProductsGetdataProcessor{ protected $modelsIDs = array(); public function initialize() { $this->setDefaultProperties(array( 'sort' => "Models.menuindex", )); return parent::initialize(); } // Готовим запрос на выборку уникальных объектов // Нам надо вернуть товары с приоритетом на наличие картинки protected function PrepareUniqObjectsQuery(xPDOQuery &$query) { $query = parent::PrepareUniqObjectsQuery($query); /* * Без изврата с подзапросом не обошлось, так как group by parent не учитывает * сортировку по картинке. */ $IDs = array(); $sub_query = clone $query; $sub_query->select(array( "{$this->classKey}.parent", )); // Сортируем по картинкам, а то могут попасть без картинки $sub_query->leftJoin('modTemplateVarResource', 'image', "image.contentid = {$this->classKey}.id and image.tmplvarid=8 and image.value != ''"); $sub_query->sortby('image.id', "DESC"); // Сбрасываем лимиты, так как нам нужные все возможные ID-шники, так как у нас нет группировки в // этом запросе $sub_query->query['offset'] = 0; $sub_query->query['limit'] = 0; if($sub_query->prepare() AND $sub_query->stmt->execute() AND $rows = $sub_query->stmt->fetchAll(PDO::FETCH_ASSOC)){ $parents = array(); foreach($rows as $row){ // Фиксируем только по одному ID на модель if(in_array($row['parent'], $parents)){ continue; } $parents[] = $row['parent']; $IDs[] = $row['id']; } $query->where(array( "id:IN" => $IDs, )); } // Группируем по родителям $query->groupby("{$this->classKey}.parent"); return $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()) { 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';
Без подзапроса здесь не обошлось, так как при группировке по родителю не учитывается сортировка по картинкам.
Задача вторая. JS-скрипт выбора доступных вариантов товара по параметрам.
А вот с этим я вообще более 10-ти часов сегодня просидел. ?
Суть заключается в том, что в карточке товара должна быть возможность выбрать конкретный товар по имеющимся параметрам. Но не все товары имеют все представленные характеристики, потому при выборе одних характеристик должны скрываться те, которые не актуальны для выбранных параметров (к примеру, для выбранного размера не все цвета имеются.).
Задача на самом деле имеет множество подводных камней, и думаю, здесь требуется очень хорошее знание математики. Наверняка человек, владеющий знаниями по математике на хорошем уровне, формулу бы написал за 10 минут максимум. Но это не я. В любом случае, скрипт возможно кривенький, но задачу свою решает. Вот код:
var smCart = function(options){ var self = this; this.options = options || {}; this.data = { color: null, design: null, size: null }; this.cart_variants = this.options.cart_variants; var c = console.log; this.IDs = []; // ID-шники возможных товаров по условиям выбора this.items = $('.product_var'); this.lastUsed = null; /* * Фиксируем все допустимые варианты **/ this.setAvailables = function(){ this.available = []; for(var i in this.data){ this.available[i] = []; if(!this.data[i]){ for(var x in this.cart_variants[i]){ this.ArrayMergeUnique(this.available[i], this.cart_variants[i][x]); } } else{ var el = $('[name='+i+']:checked'); this.ArrayMergeUnique(this.available[i], this.cart_variants[i][el.val()]); } } return; } this.checkAllowed = function(activeItem){ // c($(activeItem).parent().attr('class')); if($(activeItem).parent().hasClass('disabled')){ $(':checked').each(function(){ if(activeItem[0] == this){ return; }; var item = $(this); item.parent().removeClass('disabled'); self.unsetData(item.attr('name')); }); self.setAvailables(); } var elements = $('.product_var'); elements.parent().removeClass('disabled'); elements.each(function(i){ var el = $(this); // Если это текущий элемент, прерываем процесс if(activeItem[0] == this){ return; } var debug = false; var name = el.attr('name'); // design|color|size var value = el.val(); var IDs = self.cart_variants[name][value]; // Надо проверить каждый ID во всех массивах, кроме текущего var exists = false; var searched = false; var finded = false; $(IDs).each(function(y){ var id = IDs[y]; // Проходим каждый ID по порядку. // Найден должен быть во всех разделах // Проверяем для конкретного id var id_finded = false; for(var x in self.available){ if(!self.available[x].length || x == name // || !self.data[x] ){ continue; } id_finded = false; if($.inArray(id, self.available[x]) == -1){ id_finded = false; break; } id_finded = true; } if(id_finded == true){ finded = true; return false; } }); if(finded == false){ // Если это отмеченный ранее элемен, снимаем значение if(el.attr('checked')){ self.unsetData(name); self.setAvailables(); self.checkAllowed(activeItem); return; } else{ el.parent().addClass('disabled'); } } }); return; } this.onChange = function(e){ var item = $(this); var name = item.attr('name'); var value = item.val(); // Фиксируем новое значение поля self.data[name] = value; // Набиваем массив всех допустимых вариантов self.setAvailables(); self.checkAllowed(item); return; }; // Мержим уникальный массив this.ArrayMergeUnique = function(array1, array2){ $(array2).each(function(i){ if($.inArray(array2[i], array1) == -1){ array1.push(array2[i]); } }); return array1; } // Снимаем отметку с элемена this.unsetData = function(name){ $('[name='+ name +']:checked').attr('checked', false); this.data[name] = null; } this.items.bind('change', this.onChange); };
Конечно его надо будет еще подправить, но в целом я им доволен :-) Он получился довольно универсальным, и отыгрывает любые варианты с товарами.
Вот же ж блин! И ведь правда сработало! Как я мог забыть про это? Спасибо огромное!
Вот некогда особо вчитываться, хотя топик интересный. Я думаю, вас спасет один простенький финт: в PHP-условиях отказаться от ассоциативных массивов, а писать просто
$q->where(array( "моя длинная строка-запрос вместе с > и т.п.", ));
Тогда xPDO не будет парсить эту строку и манипулировать с ней.
Народ, всем привет! Столкнулся тут с такой/ими ситуациями и не знаю что делать. В общем xPDO: 1. обрезает длинное WHERE-условие; 2. автоматически добавляет название класса в WHERE когда это не надо. Понадобилось мне тут посчитать расстояние между двумя точками, основанными на широте и долготе. Нагуглил умную формулу и даже SQL-реализацию. Пишу запрос:
$q = $modx->newQuery('modResource'); $q->select(array( 'modResource.id', 'modResource.pagetitle', '`TVlat`.`value` as `lat`', '`TVlng`.`value` as `lng`', '`TVprice`.`value` as `price`', '((ACOS(SIN(53.2242846 * PI() / 180) * SIN(`TVlat`.`value` * PI() / 180) + COS(53.2242846 * PI() / 180) * COS(`TVlat`.`value` * PI() / 180) * COS((50.197219700000005 - `TVlng`.`value`) * PI() / 180)) * 180 / PI()) * 60 * 1.1515 * 1.609344 * 1000) AS `radius`' )); $q->leftJoin('modTemplateVarResource', 'TVlat', 'TVlat.contentid=modResource.id AND TVlat.tmplvarid=15'); $q->leftJoin('modTemplateVarResource', 'TVlng', 'TVlng.contentid=modResource.id AND TVlng.tmplvarid=16'); $q->leftJoin('modTemplateVarResource', 'TVprice', 'TVprice.contentid=modResource.id AND TVprice.tmplvarid=3'); $q->where(array( '`TVprice`.`value`' => 5000, '((ACOS(SIN(53.2242846 * PI() / 180) * SIN(`TVlat`.`value` * PI() / 180) + COS(53.2242846 * PI() / 180) * COS(`TVlat`.`value` * PI() / 180) * COS((50.197219700000005 - `TVlng`.`value`) * PI() / 180)) * 180 / PI()) * 60 * 1.1515 * 1.609344 * 1000):<=' => 1500 // расстояние меньше полутора километра )); $q->prepare(); return $q->toSQL();
В этом запросе джойнятся 3 твшки — цена, широта и долгота. Большая и умная формула — это расчёт расстояния от определённой точки (с широтой 53.2242846 и долготой 50.197219700000005), до точки из записи таблицы. В принципе это не суть. Важно то, что запрос выйдет такой:
SELECT modResource.id, modResource.pagetitle, `TVlat`.`value` as `lat`, `TVlng`.`value` as `lng`, `TVprice`.`value` as `price`, ((ACOS(SIN(53.2242846 * PI() / 180) * SIN(`TVlat`.`value` * PI() / 180) + COS(53.2242846 * PI() / 180) * COS(`TVlat`.`value` * PI() / 180) * COS((50.197219700000005 - `TVlng`.`value`) * PI() / 180)) * 180 / PI()) * 60 * 1.1515 * 1.609344 * 1000) AS `radius` FROM `modx_do_dbsite_content` AS `modResource` LEFT JOIN `modx_do_dbsite_tmplvar_contentvalues` `TVlat` ON TVlat.contentid=modResource.id AND TVlat.tmplvarid=15 LEFT JOIN `modx_do_dbsite_tmplvar_contentvalues` `TVlng` ON TVlng.contentid=modResource.id AND TVlng.tmplvarid=16 LEFT JOIN `modx_do_dbsite_tmplvar_contentvalues` `TVprice` ON TVprice.contentid=modResource.id AND TVprice.tmplvarid=3 WHERE ( `((ACOS(SIN(53`.`2242846 * PI() / 180) * SIN(`TVlat` <= '1500' AND `TVprice`.`value` = '5000' )
Всё внимание на WHERE:
WHERE (`((ACOS(SIN(53`.`2242846 * PI() / 180) * SIN(`TVlat` <= '1500' AND `TVprice`.`value` = '5000' )
xPDO беспардонно обрезал необходимое мне условие :-(
А почему в условии просто не написать WHERE `radius` <= '1500'? Спросите вы… Зачем понадобилось дублировать формулу в WHERE? А потому, что mysql парсит и обрабатывает запрос справа налево, и в момент, когда, он встречает в условии `radius` — он ещё не знает про этот алиас, а поля такого не существует. Соответсвенно, мускул кидает ошибку.
Но есть и второй вариант с HAVING'ом — это то же самое, что и WHERE, но для вычисляемых полей, таких как SUM, AVG или как моего. И из-за особенностей всё того же mysql'я, в конструкции HAVING можно писать алиас, потому что HAVING обрабатывается позже SELECT'а. Такие дела.
Поэтому переписываю запрос вот так:
$q = $modx->newQuery('modResource'); $q->select(array( 'modResource.id', 'modResource.pagetitle', '`TVlat`.`value` as `lat`', '`TVlng`.`value` as `lng`', '`TVprice`.`value` as `price`', '((ACOS(SIN(53.2242846 * PI() / 180) * SIN(`TVlat`.`value` * PI() / 180) + COS(53.2242846 * PI() / 180) * COS(`TVlat`.`value` * PI() / 180) * COS((50.197219700000005 - `TVlng`.`value`) * PI() / 180)) * 180 / PI()) * 60 * 1.1515 * 1.609344 * 1000) AS `radius`' )); $q->leftJoin('modTemplateVarResource', 'TVlat', 'TVlat.contentid=modResource.id AND TVlat.tmplvarid=15'); $q->leftJoin('modTemplateVarResource', 'TVlng', 'TVlng.contentid=modResource.id AND TVlng.tmplvarid=16'); $q->leftJoin('modTemplateVarResource', 'TVprice', 'TVprice.contentid=modResource.id AND TVprice.tmplvarid=3'); $q->where(array( '`TVprice`.`value`' => 5000, )); $q->having(array( '`radius`:<=' => 1500 // расстояние меньше полутора километра )); $q->prepare(); return $q->toSQL();
И что мы видим?
SELECT modResource.id, modResource.pagetitle, `TVlat`.`value` as `lat`, `TVlng`.`value` as `lng`, `TVprice`.`value` as `price`, ((ACOS(SIN(53.2242846 * PI() / 180) * SIN(`TVlat`.`value` * PI() / 180) + COS(53.2242846 * PI() / 180) * COS(`TVlat`.`value` * PI() / 180) * COS((50.197219700000005 - `TVlng`.`value`) * PI() / 180)) * 180 / PI()) * 60 * 1.1515 * 1.609344 * 1000) AS `radius` FROM `modx_do_dbsite_content` AS `modResource` LEFT JOIN `modx_do_dbsite_tmplvar_contentvalues` `TVlat` ON TVlat.contentid=modResource.id AND TVlat.tmplvarid=15 LEFT JOIN `modx_do_dbsite_tmplvar_contentvalues` `TVlng` ON TVlng.contentid=modResource.id AND TVlng.tmplvarid=16 LEFT JOIN `modx_do_dbsite_tmplvar_contentvalues` `TVprice` ON TVprice.contentid=modResource.id AND TVlng.tmplvarid=3 WHERE `TVprice`.`value` = '5000' HAVING `modResource`.`radius` <= '1500'
Снова внимание в WHERE:
WHERE `TVprice`.`value` = '5000' HAVING `modResource`.`radius` <= '1500'
К псевдониму `radius` xPDO добавил `modResource`, хотя он здесь не нужен и я его об этом не просил. Соответственно результат снова пустой :-(
К слову сказать, все запросы рабочие и проверены на живой базе. Осталось только воспроизвести их через построитель запросов xPDO. Блин, ну почему он так делает? Как это побороть?
А вот это очень полезная штука. Спасибо, вот и тема, которую можно потыркать на праздниках)
Так вот это и есть данные, то есть пользователь успешно идентифицирован. Теперь просто добавляйте сюда связанные таблицы по айдишникам. К примеру, Geoip_country_index — это индекс страны. В данном случае 1. Смотрите в таблице стран. А Geoip_city_index в таблице городов. Кстати, судя по всему вы использовали запрос через $query. Но $location = Geoip::findByIp($modx); возвращает уже более подробную информацию. Посмотрите код в geoip.class.php, там же все расписано. К примеру этот метод:
public static function findByIp(xPDO $xpdo, $ip = 'auto'){ if(!$c = self::newQuery($xpdo, $ip)){ return false; } $c->leftJoin('GeoipLocation', 'Location'); $c->leftJoin('GeoipCountry', 'Country'); $c->select(array( "Country.*", "Location.*", "Geoip.*", "ip_end-ip_start as distance", )); $c->sortby('distance'); $c->limit(1); return $xpdo->getObject("Geoip", $c); }
Вот вам подробнейший пример связанных запросов.
был бы вам признателен за подробный пример… единственное что мне удалось так это получить массив )
Array ( [0] => Array ( [Geoip_id] => 11206 [Geoip_ip_start] => -734667007 [Geoip_ip_end] => -734666752 [Geoip_city_index] => 2097 [Geoip_country_index] => 1 ) )
Сегодня вкратце расскажу о том, как я решал одну непростую задачку. Суть ее заключается в том, что мне надо было создать большую таблицу (98 колонок), но не просто так, а из полей имеющейся формы на одном сайте. То есть там уже есть имена полей (в итоге — колонок), лейблы этих полей (они пойдут в комменты колонок и человекопонятные названия полей в итоговой форме).
Само собой вручную создавать таблицу, все 98 колонок и т.п. — это во-первых, лениво, а во-вторых, не очень продуктивно. Я решил все это сделать программно. Вот это здесь и опишу вкратце.
Краткий план работ.
План простой:
  1. Посредством jQuery собрать информацию о полях (названия, лейблы, дефолтовые значения), упаковать это все в JSON.
  2. Средствами PHP на своем уже сервере преобразовать JSON в массив исходных данных. Сгенерировать и записать модель xPDO-объекта
  3. Из описания xPDO-объекта создать конечную таблицу.
Этап первый. Собираем данные формы и упаковываем в JSON.
В FireBug-е на нужной мне странице выполнил такой код (jQuery там был, что не удивительно):
var fieldsSer = $('Form').serializeArray(); // Собираем все поля формы var fields = {}; var name; for(var i in fieldsSer){ el = fieldsSer[i]; name = el.name fields[name] = el; // Находим лейбл для поля (у всех полей id совпадало с name, потому было просто найти лейблы для них) label = $('[for='+ name +']') text = label.text(); fields[name].label = text; } // Собираем полученный объект в JSON JSON.stringify(fields);
Этап второй. Из JSON-строки формируем
<?php $className = 'myClass'; $fields = array(); $fieldMeta = array(); // Формируем путь до map-файла, в который будем записывать конечные данные $file = MODX_CORE_PATH .'components/mypackage/model/mypackage/mysql/myclass.map.inc.php'; $arr = $modx->fromJSON($JSON); // Преобразуем JSON-строку в массив // Обрабатываем все поля foreach($arr as $field => $meta){ $value = $meta['value']; $dbtype = "varchar"; $phptype = "string"; $precision = 255; $null = true; $required = false; if(is_numeric( $value)){ $dbtype = "int"; $phptype = "integer"; $precision = 11; } if($value == ''){ $value = NULL; } $fields[$field] = $value; $label = trim($meta['label']); $label = preg_replace("/[\r\n]+/", "", $label); $label = preg_replace("/ {2,}/", " ", $label); if(!empty($label) && strpos($label, '*')){ $required = true; } $fieldMeta[$field] = array( 'comment' => $label, 'dbtype' => $dbtype, 'precision' => $precision, 'phptype' => $phptype, 'null' => $null, 'default' => $value, 'required' => $required, 'in_form' => true, ); } /* Готовим и записываем конечное описание объекта */ $xpdo_meta_map = array ( 'package' => 'myPackage', 'version' => '1.1', 'table' => 'mytable', 'extends' => 'xPDOSimpleObject', 'fields' => $fields, 'fieldMeta' => $fieldMeta, ); // Обратите внимание на функцию var_export - очень важная и полезная штука $fileContent= "<?php\n\$xpdo_meta_map['$className']= " . var_export($xpdo_meta_map, true) . ";\n"; // Записываем полученный массив в файл $modx->cacheManager->writeFile($file, $fileContent);
Этап третий. Из модели объекта создаем таблицу.
Здесь уже все просто и стандартно.
$manager = $modx->getManager(); // Создаем таблицу объекта // Таблицы не должно быть изначально, этот метод только создает новую таблицу, // не обновляет и не грохает имеющуюся $manager->createObjectContainer('myClass');
Немного лирики.
Вообще описанный метод для меня сегодня был практически единственным приемлемым в моей ситуации. У меня многое в проекте завязано именно на map-описании объекта (Туда же записать можно все, что угодно. В частности, у меня там записаны комментарии, которые в итоге подставляются в названия полей в форме, а в базе данных они просто необходимы, чтобы разобраться в массе всяких «интуитивно-понятных» полей). И у меня был вариант или создавать таблицу, и потом из нее генерить map-файл, или наоборот, сначала создать map-файл, а потом уже создать таблицу. Я выбрал именно второй, так как описанное здесь — это только начало. Еще предстоит пройтись по всем полям, актуализировать типы данных до конца, указать длину полей и т.д. и т.п. И в этом плане с файлом работать гораздо проще, чем с таблицей, к тому же еще и гораздо быстрее. А ничего не стоит на этапе разработки грохнуть всю таблицу и в две строчки создать новую.
А еще следует сразу отметить, что в xPDO имеются методы изменения таблиц, удаления и добавления колонок и т.п. Но это уже тема для отдельного топика.