Плагин TagSaver для работы с тегами

Вообще, фильтрация по тегам в MDOX реализуется довольно кисло. Представьте, у вас есть какой-то TV параметр, в котором у вас к каждой статье прописано по 10 тегов.

А в базе все эти теги для документа хранятся в одной строке. Т.е. Выглядит это примерно так
modx, теги, работа с тегами, фильтрация по тегам

Как обычно происходит фильтрация? Создается SQL запрос в таблицу site_tmplvar_contentvalues с применением WHERE value LIKE %тег%
Таким образом, мы можем искать не только по тем тегам, которые задумывались автором сайта, но и по тегам
  • и, р
  • ми, фи
  • льтра
  • бот
  • фил
  • mod
Более того, если документов очень много, то поиск происходит нереально долго. Как вообще идеально должна выглядеть работа с тегами?
Таблица с документами вида: id, content, pagetitle
Таблица с тегами: id, tag
Таблица связей с тегами: doc_id, tag_id
Можно конечно долго спорить о необходимости таблицы связи, т.к. придется делать лишний JOIN. Но за счет правильно расставленых индексов этот джоин вообще незаметен. Более того, немного доработав таблицу мы можем забивать теги в нескольких TV-шках, а хранить в одной таблице по вышеописаному стандарту.
На основании всего вышесказанного у нас получаются вот такие 2 таблицы

DROP TABLE IF EXISTS `modx_site_content_tags`;
CREATE TABLE `modx_site_content_tags` (
  `doc_id` int(11) NOT NULL,
  `tag_id` int(11) NOT NULL,
  `tv_id` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`tag_id`,`doc_id`,`tv_id`),
  UNIQUE KEY `dtt` (`doc_id`,`tag_id`,`tv_id`) USING BTREE,
  KEY `doc_id` (`doc_id`),
  KEY `tag_id` (`tag_id`),
  KEY `tv_id` (`tv_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `modx_tags`;
CREATE TABLE `modx_tags` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;


Как можно заметить, формат у таблиц MyISAM. Но у такого выбора есть своя причина. Одна из них это auto_increment. А вторая — уникальный ключ по нескольким полям. Не буду долго задерживаться на этом пункте. Скажу одно — если у вас MODX установлен с префиксом таблиц отличным от modx, то измените его на свой;-)

Создали таблицы? И что? Как теперь инфу то в них добавлять?
Все очень просто. Создаете как обычно TV параметр. Можете даже добавить виджет mm_widget_tags от ManagerManager. После этого создаете плагин TagSaver с параметрами
&tv=ID TV-параметра;input; &sep=Разделитель тегов;input;

Если разделителя нет, то можно оставить пустым.

Что делать если мне нужно 2 поля с тегами?
Создавайте копию плагина под другим именем с аналогичными параметрами. Только не забудьте у новой версии плагина указать разделитель и ID другого TV параметра.

Почему плагин делает SELECT за значениями TV, а не принимает $_POST['tv'.$tv]?
  1. Мы работаем в админке и ± 1 SQL запрос особого вреда не принесет
  2. Могут стоять другие плагины которые вызываются раньше и изменят значение сохраняемого TV параметра
  3. Значение TV параметра из $_POST может отличаться от сохраненного в базу

Как вывести на странице теги привязаные к текущей стрнице?
Выводить их можете как и раньше — через сниппеты DocInfo, просто подставляя tv параметр на странице [*tag*].

Как теперь отфильтровать данные по нужному тегу?
Очень просто. Создаете сниппет примерно такого содержания
$tag=((isset($tag) && is_scalar($tag))? $tag : (isset($_GET['tag']) && !is_array($_GET['tag']) ? $_GET['tag'] : ''));
$id=isset($id) ? (int)$id : 0;
$out=array();
if($id>0 && $tag!=''){
	$sql=$modx->db->query("SELECT doc_id FROM {$modx->getFullTableName("tags")} as t LEFT JOIN {$modx->getFullTableName("site_content_tags")} s ct ON ct.tag_id=id WHERE t.`name`='{$modx->db->escape($tag)}' AND ct.tv_id={$id}");
	$sql=$modx->db->makeArray($sql);
	foreach($sql as $item){
		$out[]=$item['doc_id'];
	}
}
return implode(",",$out);


И результат работы этого сниппета передаете в параметр documents от Ditto. У сниппета указаного выше есть 2 параметра:
  • tag — если этот параметр передан, то сниппет использует его значение. В противном случа пытается взять значение $_GET переменной tag
  • id — Идентификатор TV переменной в которой хранятся теги

Получается примерно так [[Ditto? &documents=`[!GetTag? &id=`2`!]`]]

Как теперь вывести популярные теги?
$count = isset($count) ? (int)$count : 10;
  $tv = isset($tv) ? (int)$tv : '';
  $out = array();
$sql=$modx->db->query("SELECT ct.tv_id,t.name,count(ct.tag_id) as count FROM ".$modx->getFullTableName("tags")." as t LEFT JOIN ".$modx->getFullTableName("site_content_tags")." as ct ON ct.tag_id=t.id LEFT JOIN ".$modx->getFullTableName("site_content")." as c on c.id=ct.doc_id WHERE deleted=0 AND published=1 ".(($tv!='') ? ("AND ct.tv_id=".$tv) : "")." GROUP BY tag_id ORDER BY count(ct.tag_id) DESC LIMIT 0,".$count);
 $sql=$modx->db->makeArray($sql);
foreach($sql as $item){
  $out[]=$modx->parseChunk($tpl,$item,"[+","+]");
}
return implode((isset($outSep) ? $outSep : ""),$out);

У сниппета 3 параметра
  • count — сколько тегов выводить (по умолчанию 10)
  • tv — Идентификатор TV переменной в которой хранятся теги
  • tpl — чанк в который будут подставляться теги. У меня он выглядит так
    <a href="/[~11~]?tag=[[urlencode? &input=`[+name+]`]]" title="Статьи с тегом [+name+]" class="label">[+name+] ([+count+])</a>
Как видно, этот сниппет не только теги подставляет, но еще и считает сколько раз они используются. Таким образом вызов сниппета для облака тегов выглядит так:
[[TagCloud? &tpl=`TagCloudItem` &tv=`7` &count=`20`]]


А что за сниппет urlencode?

return isset($input) ? urlencode($input) : '';

Для чего он нужен найдете в гугле

В силу того, что мне довольно часто приходится работать с сайтами сделаными другими программистами, то могу сказать вам одно — таких решений еще нигде небыло. Все фильтруют по старинке средствамми Ditto и изобретают свои велосипеды в виде экстендеров для него. Мое решение повзоляет вам работать с видимой частью так же, как и раньше. Зато существенно снимает нагрузку на сервер при фильтрациях. И облегчает выборку даже если вы используете знаменитый сниппет DropDownDocs для привязки статей к нескольким разделам (ведь по сути это тоже теги, только вид с боку). Соответственно, если вам нужны теги и фильтрации по TV параметрам в значении которых может быть несколько данных — используйте TagSaver.

38 комментариев

avatar
Демонстрацию работы плагина можно посмотреть на блоге для худеющих мужчин)) Сайт пока не доделан, т.к. руки не доходят.
avatar
Кстати, разделы тоже реализованы при помощи плагина TagSaver с разделителем || и сниппета DropDownDocs привязанного к TV параметру для выбора нужного раздела.
Комментарий отредактирован 2013-02-01 16:54:08 пользователем Agel_Nash
avatar
Как оно кстати -))
как раз нужно было теги вывести, сегодня опробую
avatar
Очень интересно узнать результаты, т.к. с Ditto я не тестировал( Набросал пример по ходу написания статьи.
avatar
Пошел смотреть плагин, а заинтересовался контентом. О результатах отпишусь )))
avatar
Где результаты?
avatar
Результаты есть, я проникся и теперь делаю зарядку два раза в день)
avatar
Ну как тесты?
avatar
у меня неполучилось вывести, пока не розбирался почему невыводит, времени небило
avatar
Плагин работает. сохраняет в таблицу значения. все ок. А вот со сниппетом GetTag проблема. судя по всему не может взять значения из таблицы. Если вызываем
[[Ditto? &documents=`[!GetTag? &id=`2`!]`]], все валится. [!Ditto? &documents=`[!GetTag? &id=`2`!]`!] так просто выводит 'записей не найдено'. Я так понял сниппет этот должен вывести id документов, к прим.- '1,2,3' через запятую, но не выводит. modx evo 1.0.13 от Дмитрия.
Комментарий отредактирован 2014-01-24 19:29:22 пользователем serge
avatar
Для DocLister'a есть контроллер site_content_tags

[!DocLister?  &controller=`site_content_tags` &tagsTV=`7` &tagsData=`get:tag`!]

где tagsTV это ID TV параметра с тегами, а tagsData определяет от куда взять значение тега. В данном случае из $_GET['tag']. Можно еще так написать
&tagsData=`static:привет`

Будут найдены все записи с тегом «привет»
avatar
Что-то не хочет у меня фильтровать таким образом. Как только добавляю параметр &tagsData=`static:word` перестают выводиться документы, хотя документы с тегом word есть
[!DocLister? &display=`10` &id=`taglist` &parents=`4` &controller=`site_content_tags` &tpl=`taglistPost` &tagsTV=`5` &tagsData=`static:word`!]

ни static не хочет работать ни get :(
Комментарий отредактирован 2014-11-28 14:04:55 пользователем lex
avatar
debug=1 какие запросы показывает?
avatar
action time: 0,00023 · total time: 0,00604

SELECT id, name FROM `yatra`.`yatra_site_tmplvars`

action time: 0,00035 · total time: 0,00642

SELECT c.* FROM `yatra`.`yatra_site_content` as `c` RIGHT JOIN `yatra`.`yatra_site_content_tags` as `ct` on ct.doc_id=c.id RIGHT JOIN `yatra`.`yatra_tags` as `t` on t.id=ct.tag_id WHERE t.`name`='word' AND c.parent IN ('4') AND c.id NOT IN('4') AND c.deleted=0 AND c.published=1 GROUP BY c.id ORDER BY if(c.pub_date=0,c.createdon,c.pub_date) DESC LIMIT 0,10
avatar
Запрос верный. Значит нужно смотреть есть ли данные в таблице yatra_tags.

Если плагин устанавливали уже на рабочий сайт с тэгами, то документы необходимо пересохранить.
avatar
Таблица пустая, пошел смотреть плагин
UPD.
Моя ошибка. Не стояла галка на событии у плагина. Поставил галку, пересохранил документы — теперь все работает.
Комментарий отредактирован 2014-11-28 18:07:12 пользователем lex
avatar
в вызове опечатка, лишняя буква s после названия таблицы
$modx->getFullTableName("site_content_tags")} s ct ON ...
avatar
Как вывести на странице теги привязаные к текущей странице?
Выводить их можете как и раньше — через сниппеты DocInfo, просто подставляя tv параметр на странице [*tag*].
Возможно я чего-то здесь не понимаю, — ведь так будет выведен просто плоский список без ссылок? (переменная tag — одно текстовое поле, список с разделителем)
Как преобразовать вывод переменной tag в список с ссылками?
avatar
Отвечу сама себе, вдруг кому пригодится. Плагин tagLinks (устанавливается через Extras), в коде сниппета
/**
* Build the URL
*/
$url_seperator = (strpos($path, '?') === false)?'?':$amp;
$doc_path = $path.$url_seperator.$idDitto.'tag=';

поменять 'tags=' на 'tag=', и решение готово.
Комментарий отредактирован 2014-09-22 07:58:24 пользователем Jassie
avatar
Есть шаблон(Блог внутреняя) --> к этому шаблону присвоен TV параметр (tags)
пробую вывести теги следующим запросом


[!DocLister? 
&controller=`site_content_tags`
&ownerTPL=`@CODE:<ul class="tag">[+dl.wrap+]</ul>`
&tpl=`@CODE:<li><a class="color" href="[+url+]">[+title+]([+id+])<span>/</span></a></li>`
&tagsData=`get:[*tags*]`
&debug=`1`
&id=`tags` 
&tagsTV=`7` !]


В итоге нечего не выводит! При отладке выдает следующее


SELECT 
  id, 
  name 
FROM 
  `kuponik_ali`.`modxs_site_tmplvars`

SELECT 
  c.* 
FROM 
  `kuponik_ali`.`modxs_site_content` as `c` 
WHERE 
  c.parent IN ('9') 
  AND c.id NOT IN('9') 
  AND c.deleted = 0 
  AND c.published = 1 
GROUP BY 
  c.id 
ORDER BY 
  if(
    c.pub_date = 0, c.createdon, c.pub_date
  ) DESC


Как вывести теги к документу?
avatar
Работает, спасибо.
Запустилось сразу: TagSaver + DocLister + TagCloud + tagLinks
avatar
Добрый день, возник такой вопрос — надеюсь на помощь.

Суть проблемы.
Делаю интернет-магазин, в котором загрузка товаров идет через импорт (модудь catalogFill). Товаров — пока тысяч 10. Хотел реализовать с помощью TagSaver функцию поиска по брендам, но получился облом — никто 10 тыс страниц пересохрянять не будет, а без этого не срабатывает:
— в таблицу modx_site_tmplvar_contentvalues бренды при импорте записываются, а в таблицы modx_tags и modx_site_content_tags — нет, соответственно и не работает.

Подскажите или пихните, как решить эту проблему.

Спасибо.
avatar
Просто вызвать каким-нибудь сниппетом событие onDocFormSave для каждого документа. Что-то вроде такого
<?php
// 10 - id шаблона товара
$q = $modx->db->query("SELECT id FROM modx_site_content WHERE template=10");
while ($row = $modx->db->getRow($q)) {
    $modx->invokeEvent("OnDocFormSave", array('mode' => 'upd', 'id' => $row['id']));
}


и где-нибудь его однократно запустить.
Комментарий отредактирован 2016-06-01 18:36:07 пользователем webber
avatar
Спасибо!
Еще бы придумать что-нибудь более удобное и понятное для менеджера))
avatar
Загнать все то же самое в модуль с одним полем hidden name=«renew» value=«1» и кнопкой обновить

if (isset($_POST['renew'])) {… }
avatar
Спасибо!
avatar
Спасибо webber , родился вот такой модуль, с помощью которого пересохраняются все страницы товаров, импортируемые на сайт, и тем самым производится запись в таблицы TagSaver

if (isset($_POST['renew'])) {
	// 8 - id шаблона товара
	$q = $modx->db->query("SELECT id FROM modx_site_content WHERE template=8");
	while ($row = $modx->db->getRow($q)) {
        $modx->invokeEvent("OnDocFormSave", array('mode' => 'upd', 'id' => $row['id']));
	}
    echo "<b>Обновление данных прошло успешно</b>";
}
else{
    $renew_url=$_SERVER['REQUEST_URI'];
    echo "<form action=\"".$renew_url."\" method=\"POST\">
	<h2>Управление брендами на сайте</h2>
        <input type=\"hidden\" name=\"renew\" value=\"1\"/><br />
        <input type=\"submit\" value=\"Обновить бренды\" />
        </form>";
}
avatar
У меня модуль говорит вот такое

Execution of a query to the database failed — Table 'здесь имя базы.modx_site_content' doesn't exist

Пардон, имя таблицы, смотрю, захардкодено.
Но мне не помогло. Неиспользуемые теги остались висеть в облаке

UPD Были два былинных тега «новости» и «событие» в таблице _tags и в таблице _site_tags были несколько документов с ними, но у них был не тот шаблон, поэтому, собственно, они вообще не сбрасывались
Комментарий отредактирован 2016-08-16 15:07:36 пользователем alexbeep
avatar
Подскажите не могу разобраться с TagSaver!
Через TvTagCloud вывожу список тегов, при клике по тегу к url добавляется ?tags=555
но в результате выводиться &noneTPL, а не результат!

DocLister следующий:
[[DocLister?
&idType=`parents`
&parents=`96`
&tvList=`autorTV`
&tvPrefix=``
&controller=`site_content_tags`
&tagsTV=`10`
&tagsData=`get:tags`
&noneTPL=`@CODE: :(`
&tpl=`statti-block`
&id=`statti`
&depth=`3`
&display=`15`
]]

Что делаю не так?
avatar
Пробовал использовать следующую конструкцию в выводи DocLister на странице:
&tagsData=`get:[*tags*]` — подставляя и имя TV и id DocLister (подсмотрел в одном из комментариев выше) — но таким образом результат не меняется!
&tagsData=`static:555` тоже нечего не выводит, как по мне проблема с базой почему-то у меня таблица modx_site_content_tags — пустая пробовал добавить вручную данные что бы проверить, но что-то не получается. И не могу понять в чем причина!
avatar
Лучшее решение по выводу и фильтрации тегов
avatar
Начал выводить теги, но что-то не могу разобраться с выводом тегов по отдельности как в примере joxi.ru/YmEY8BDIXpgem6 где каждый тег отдельная ссылка. У меня получается только все теги в одну ссылку. Подскажите плз как это сделать?
avatar
Добрый день. Пожалуйста подскажите.
Работает связка TagSaver + DocLister.
Вызов доклистера6
[[DocLister?
&controller=`site_content_tags`
&idType=`parents`
&parents=`2`
&tagsTV=`13`
&tagsData=`get:tags`
&noneTPL=`@CODE: ОШИБКА :(`
&tpl=`@CODE: tmpl`
]]

по вызову ?tags=Логотип абсолютно правильно выводится выборка по «Логотип» (если он отдельно), но если «логотип» находится в составе других, например «Логотип, Упаковка» уже игнорируется.
Или это не сделать через &controller=`site_content_tags`, а надо через фильтры например?
  • CheD
  • 0
avatar
Потому что get:tags выводит по одному тэгу, а не нескольким.
avatar
так мне и надо по одному. Надо чтобы по ОДНОМУ тегу, например «Логотип» производился поиск. Но не только в тех позициях где он присутствует в единичном числе, но и по тес где он вместе с другими тэгами
Вот кусочек таблицы modx_tags

DocLister выводит только те позиции где id=9. И пропускает 12, 10, 8 и т.д. хотя там тоже есть нужный тег.
Или я както неверно понял логику TagSaver'a
avatar
Тег в вашем случае — это полная строка. Равенство этой строке и ищется. Частями строка не ищется — вам надо изменить логику создания тегов — например поставить разделитель запятую вместо ||, чтобы у вас был отдельный тег упаковка и отдельный тег логотип. Тогда будет искать и там, где задан только «логотип», и там, где задан «логотип, упаковка» в TV тегов
avatar
Т.е. не должно быть тега «Наружная реклама, Вывеска, Логотип» — это три разных тега «Наружная реклама», «Вывеска», «Логотип». Тогда данный документ будет выдаваться по всем трем запросам. А для этого надо поставить разделитель запятая в параметрах плагина.
avatar
Да, спасибо. Из документации эта функция разделителя как-то не очевидна. Не дошло)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.