MODX - Quip(система комментов. переделка под "Вопрос-Ответ")

ВВедение


Quip написан очень давно. Одним из основателей MODX. Тут все четко и по делу. Если просто надо добавить комменты к странице - вообще никаких проблем. И комменты к этой статье - Quip. Причем установил я его неделю спустя, как впервые узнал про MODX. Тупо вставил рекондуемые пару тэгов. И все! В "одном флаконе" вы получаете и систему управления в админке. Но мне, как часто это бывает, везет на нестандартные требования клиентов. Вот поступил заказ перевести сайт с Эво на Рево. На старом сайте стоит система "Вопрос - ответ". На JotX.  Почитав доки по Quip, поначалу думал поискать что-то более современное и навороченное. Но с другой стороны еще осталась память от "корячанья" с JotX. И к тому же очень мне понравились исходники Quip. И организация кучков кода. Все как надо в MODX Revo. Коннекторы, процессоры, модели и прочие навороты Revo. Все действия реализованы отдельными компактными по размеру классами. Но не нашел я в доках даже упоминания о хуках(обратки, функции обратного выхова, функции юзера. ну кому надо - тот поймет). Т.е. не хватает того, что реализовано в JotX через конфигурации и события, где можно расширять и изменять функционал, не меняя исходник.  Но оказалось, что в Quip не так прост. Что очень обрадовало. Есть хуки! Но почему-то недокументированы. И аналог конфигуратора есть. Но неявный. Тут разные блоки вызываются отдельными сниппетами. Т.е. отдельно форма, отдельно комменты. И реально получается очень крутая и гибкая вещь. Как всегда пишу в стиле "live", т.е реально это конспект при разработке системы "Вопрос - ответ"  в MODX Revo на Quip.

Организация кода и применение.


Применение очень удобное. В последнее время активно различные раширения используют вызов сниппета при различных конфигурациях, которые передаются как параметр. Это очень удобно для программиста и поэтому я сначала расстроился, что нет такого у Quip. Оказалось напрасно. Тут оказалось все круче). А дело в том что Quip - набор сниппетов и у каждого свой набор параметров. Причем параметры эти назначаются через набор парметром. Что чрезвычайно удобно. Признаюсь, по-началу я этого не оценил. Но это реально очень удобно. Можно создать сколько угодно таких наборов и подключать любой из них при вызове сниппета. Мало того - если помимо набора параметров передать обычный параметр - он "перебьет" одноименный параметр из набора. Что делает применение наборов чрезвычайно гибким. 

Во-вторых Quip - это на самом деле набор сниппетов. Да что расписывать? По виду их сразу многое проясняется:

  • Quip - вывод комментов.
  • QuipReply - форма для новых комметов (ответов).
  • QuipCount - выводит кол-во комментов.
  • QuipLatestComments - вывод последних коментов пользователя или "нити".
  • QuipRss - может публиковать, например, последние комменты в RSS.

Т.е. логика такая: надо опубликовать комменты - вызывается соответствующий сниппет. Надо форму - другой сниппет. Вроде все просто, но очень гибко. В других расширениях для подобного подключают плейсхолдеры и все это превращается в трудно читаемую "кашу". 

 

 

 

 

Hooks!


Хотя в доках про них ни слова, но они есть! И это очень важно. Их, как и положено, 2 вида. Прехуки и постхуки. Первые срабатывают до публикации, вторые после публикации.

  • запускаются из процессора create
  • параметры
    • $hooks - список всех хуков, переданных при вызове quip
    • $fields - массив полей формы, причем в этом массиве - ссылки на поля. Т.е. можно понапихать "виртуальных" или поменять значения. 
      • для постхуков тут все поля основной таблицы Quip(modx_quip_comments)
      • для прехуков в основном поля формы
        $fields=Array
        (
            [nospam] => 
            [thread] => QR-1
            [parent] => 0
            [auth_nonce] => 
            [preview_mode] => 
            [name] => testAset
            [email] => cpp_v@mail.ru
            [comment] => eere er wer ewrew5555555555555555
            [quip-post] => 1
            [website] => 
            [body] => eere er wer ewrew5555555555555555
        )
        

         

  • запускаются либо встроенные (в классе quipHooks), либо сниппеты.
  • в хуке доступны переменные:
    • $quip - ссылка на объект Quip
    • $hook - ссылка на объект quipHooks
    • $fields - ссылка на поля формы
    • $errors - ссылка на массив ошибок
  • возвращать надо или true или false. False в прехуке прерывает работу процессора и процессор возвращает массив с ошибками. При возврате false любым хуком, выполнение остальных прекращается.

Это все прекрасно. Но никто не застрахован от ошибок. При подключении прехука, тот срабатывал. Но все поля формы после этого обнулялись. Причина - ошибка в процессоре. Вместо полей в прехук передаются параметры вызова сниппета. Пришлось править процессор ".../core/components/quip/processors/web/comment/create.php"

$quip->preHooks->loadMultiple($this->getProperty('preHooks',''),$this->getProperties(),array(
'hooks' => $this->getProperty('preHooks',''),
));  

я заменил на

$quip->preHooks->loadMultiple($this->getProperty('preHooks',''),$fields,array(
'hooks' => $this->getProperty('preHooks',''),
));

и все заработало.

 

Страница "Задать вопрос"


Понятно, что тут достаточно разместить на странице сниппет QuipReply . Напомню, что сниппеты в Quip идут с набором параметров по умолчанию. Повторю, что это очень удобно. Не надо лезть в доки, чтобы найти и установить нужный. Невозможно ошибиться в имени параметра. Опять же видно какое значение по умолчанию у каждого параметра. Короче - одни плюшки). Поэтому создадим свой новый параметр и будем вызывать сниппет уже с нужным нам в данной ситуации набором. Забегая вперед - нужно как минимум 2 новых параметра. 1 - для новых вопросов, второй - для ответов. Т.е. будут две формы. Поэтому я создал сразу оба. Тут не буду подробно расписывать. Это очень все просто. Итого: набор cppv_Question - для вопросов, cppv_Ansvers - для ответов.

 

 И еще одно удобство параметров - цветовая маркировка. Зеленая - дефолтное значение изменено. Фиолетовая - добавленные параметры. Пришлось добавить хуки, тему(thread) и еще пару. Некогда было автору).

Меняем нужные параметры на свои значения. Запускаем сниппет.

[[!QuipReply@cppv_Question]]

и все работает с нужными параметрами.

Поскольку я предпочитаю работать в API, то в моем случае

$propertySetObj = $modx->getObject('modPropertySet', array('name' => 'cppv_Question'));
$propertySet = $modx->parser->parseProperties($propertySetObj->get('properties'));
$out=$modx->runSnippet('QuipReply', $propertySet);

В общем форма заполняется. Почта приходит. В админке тоже все ок. 

Осталось еще сделать привязку к теме вопроса. Думаю это и будет "thread". На старом сайте для тем была создана отдельная таблица. Что повлекло за собой кучу гемороя. Это редактор таблицы и прочее сопутствующее этому. На новом сайте я решил темы сделать просто как ресурсы. Во первых их в пределах десятка - вполне потянут ресурсы. И куча плюсов. Хозяин сам сможет править заголовки, менять очередность показа, отключать. И при отображении можно использовать стандартные итераторы(getResouce, Wayfinder и прочие).

 

 

 

Фото в форме "Задать вопрос"


Задача - добавить картинку в пост. Тут есть засада. Нет возможности добавить новое поле через параметры, как в jotX. Можно, конечно, физически добавить поле, подправить модель. Но есть поле website, которое я попытаюсь ипользовать под картинку. План такой:

  • в форму добавим загрузку картинки на сайт с предпросмотром. через ajax.
    • в обработчике ajax - преместим в нужный каталог, подработаем(обрежем и прочее).
  • в постхуке - запишем в базу адрес картинки, с проверкой ее реального присутствия.

 

В реальности все оказалось проще. Сделал, чтобы виджет после предзагрузки сразу писал адрес картинки в поле "website". Теперь все ОК. Картинка грузится, база сохраняется. До кучи повысил "юзабилити". Ранее было требоване грузить картинку не более 256k. Теперь сделал до 2mb. А при предзагрузке, если картирка большая - скрипт обрезает до 240к. 

Quip (сниппет)


Сниппет вывода комментов. В моем случае это будет саисок заданных вопросов по выбранной Категории.

Важные параметры

Параметр Пояснение Значение параметра(пример)
thread нить(тема), строка, у меня 'Q-'+idKategor Q_156
parent с какого родителького id выводить комменты 5
threaded для возможности комментов на комменты, если 0 - то только комменты на parent 0
chunksPath для файловых шаблонов. имя файла - "имя_чанка_в_нижнем_регистре"+".chunk.tpl" $corePath.'elements/chunks/'

API mode


Схема пакета quip 

<?xml version="1.0" encoding="UTF-8"?>
<model package="quip" baseClass="xPDOObject" platform="sqlsrv" phpdoc-package="quip" phpdoc-subpackage="" version="1.1">
<!-- table for threads -->

<object class="quipThread" table="quip_threads" extends="xPDOObject">
  <field key="name" dbtype="nvarchar" precision="255" phptype="string" null="false" default="" index="pk" />
  <field key="createdon" dbtype="datetime" phptype="datetime" null="true" />
<field key="moderated" dbtype="bit" phptype="boolean" null="false" default="1" index="index" />
  <field key="moderator_group" dbtype="nvarchar" precision="255" phptype="string" null="false" default="Administrator" index="index" />
  <field key="moderators" dbtype="nvarchar" precision="max" phptype="string" default="" />
  <field key="notify_emails" dbtype="nvarchar" precision="max" phptype="string" default="" />

<!-- These next 3 columns are needed since Quip doesn't tie a thread to
a specific page; you have to keep a map of the Resource's ID and any
REQUEST params that are used on that page to allow for proper URL
linking. Also, you need to keep the idprefix to allow for multiple Quip
instances per page. These are auto-generated on the load of the Quip
snippet, though, so if empty they will be auto-filled. Migrators from
other comment systems need not worry to fill these in. -->

  <field key="resource" dbtype="int" phptype="integer" null="false" default="0" index="index" />
  <field key="idprefix" dbtype="nvarchar" precision="255" phptype="string" null="false" default="qcom" />
  <field key="existing_params" dbtype="nvarchar" precision="max" phptype="json" default="{}" />
  <field key="quip_call_params" dbtype="nvarchar" precision="max" phptype="json" default="[]" />
  <field key="quipreply_call_params" dbtype="nvarchar" precision="max" phptype="json" default="[]" />

  <index alias="PRIMARY" name="PRIMARY" primary="true" unique="true" type="BTREE">
     <column key="name" length="" collation="A" null="false" />
  </index>
  <index alias="moderated" name="moderated" primary="false" unique="true" type="BTREE">
     <column key="moderated" length="" collation="A" null="false" />
  </index>
  <index alias="moderator_group" name="moderator_group" primary="false" unique="true" type="BTREE">
        <column key="moderator_group" length="" collation="A" null="false" />
  </index>
  <index alias="resource" name="resource" primary="false" unique="true" type="BTREE">
        <column key="resource" length="" collation="A" null="false" />
  </index>
<aggregate alias="Resource" class="modResource" local="resource" foreign="id" cardinality="one" owner="foreign" />
  <composite alias="Comments" class="quipComment" local="name" foreign="thread" cardinality="many" owner="local" />
  <composite alias="Notifications" class="quipCommentNotify" local="name" foreign="thread" cardinality="many" owner="local" />
</object>

<!-- table for comments -->

<object class="quipComment" table="quip_comments" extends="xPDOSimpleObject">
  <field key="thread" dbtype="nvarchar" precision="255" phptype="string" null="false" default="" index="index" />
  <field key="parent" dbtype="integer" phptype="integer" null="false" default="0" index="index" />
  <field key="rank" dbtype="nvarchar" precision="512" phptype="string" />
<field key="author" dbtype="integer" phptype="integer" null="false" default="0" index="index" />
  <field key="body" dbtype="nvarchar" precision="max" phptype="text" null="false" default="" />
  <field key="createdon" dbtype="datetime" phptype="datetime" null="true" />
  <field key="editedon" dbtype="datetime" phptype="datetime" null="true" />
  <field key="approved" dbtype="bit" phptype="boolean" null="false" default="1" index="index" />
  <field key="approvedon" dbtype="datetime" phptype="datetime" null="true" />
  <field key="approvedby" dbtype="integer" phptype="integer" null="false" default="0" index="index" />
  <field key="name" dbtype="nvarchar" precision="255" phptype="string" null="false" default="" />
  <field key="email" dbtype="nvarchar" precision="255" phptype="string" null="false" default="" />
  <field key="website" dbtype="nvarchar" precision="255" phptype="string" null="false" default="" />
  <field key="ip" dbtype="nvarchar" precision="255" phptype="string" null="false" default="0.0.0.0" />
<field key="deleted" dbtype="bit" phptype="boolean" null="false" default="0" index="index" />
  <field key="deletedon" dbtype="datetime" phptype="datetime" null="true" />
  <field key="deletedby" dbtype="integer" phptype="integer" null="false" default="0" index="index" />
<!-- deprecated in 0.5.0 -->
  <field key="resource" dbtype="int" phptype="integer" null="false" default="0" index="index" />
  <field key="idprefix" dbtype="nvarchar" precision="255" phptype="string" null="false" default="qcom" />
  <field key="existing_params" dbtype="nvarchar" precision="max" phptype="json" default="[]" />

  <index alias="thread" name="thread" primary="false" unique="true" type="BTREE">
    <column key="thread" length="" collation="A" null="false" />
  </index>
  <index alias="parent" name="parent" primary="false" unique="true" type="BTREE">
    <column key="parent" length="" collation="A" null="false" />
  </index>
  <index alias="author" name="author" primary="false" unique="true" type="BTREE">
    <column key="author" length="" collation="A" null="false" />
  </index>
  <index alias="approved" name="approved" primary="false" unique="true" type="BTREE">
    <column key="approved" length="" collation="A" null="false" />
  </index>
  <index alias="approvedby" name="approvedby" primary="false" unique="true" type="BTREE">
    <column key="approvedby" length="" collation="A" null="false" />
  </index>
  <index alias="deleted" name="deleted" primary="false" unique="true" type="BTREE">
    <column key="deleted" length="" collation="A" null="false" />
  </index>
  <index alias="deletedby" name="deletedby" primary="false" unique="true" type="BTREE">
    <column key="deletedby" length="" collation="A" null="false" />
  </index>
  <index alias="resource" name="resource" primary="false" unique="true" type="BTREE">
    <column key="resource" length="" collation="A" null="false" />
  </index>

  <aggregate alias="Thread" class="quipThread" local="thread" foreign="name" cardinality="one" owner="foreign" />
  <aggregate alias="Author" class="modUser" local="author" foreign="id" cardinality="one" owner="foreign" />
  <aggregate alias="Resource" class="modResource" local="resource" foreign="id" cardinality="one" owner="foreign" />

  <aggregate alias="Parent" class="quipComment" local="parent" foreign="id" cardinality="one" owner="foreign" />
  <composite alias="Children" class="quipComment" local="id" foreign="parent" cardinality="many" owner="local" />
  <composite alias="Ancestors" class="quipCommentClosure" local="id" foreign="ancestor" cardinality="many" owner="local" />
  <composite alias="Descendants" class="quipCommentClosure" local="id" foreign="descendant" cardinality="many" owner="local" />
</object>

<object class="quipCommentClosure" table="quip_comments_closure" extends="xPDOObject">
  <field key="ancestor" dbtype="int" phptype="integer" null="false" default="0" index="pk" />
  <field key="descendant" dbtype="int" phptype="integer" null="false" default="0" index="pk" />
  <field key="depth" dbtype="int" phptype="integer" null="false" default="0" />

  <index alias="PRIMARY" name="PRIMARY" primary="true" unique="true" type="BTREE">
    <column key="ancestor" length="" collation="A" null="false" />
    <column key="descendant" length="" collation="A" null="false" />
  </index>

  <aggregate alias="Ancestor" class="quipComment" local="ancestor" foreign="id" cardinality="one" owner="foreign" />
  <aggregate alias="Descendant" class="quipComment" local="descendant" foreign="id" cardinality="one" owner="foreign" />
</object>

<object class="quipCommentNotify" table="quip_comment_notify" extends="xPDOSimpleObject">
  <field key="thread" dbtype="nvarchar" precision="255" phptype="string" null="false" default="" index="index" />
  <field key="email" dbtype="nvarchar" precision="255" phptype="string" null="false" default="" />
  <field key="createdon" dbtype="datetime" phptype="datetime" null="true" />
  <field key="user" dbtype="integer" phptype="integer" null="false" default="0" index="index" />

  <index alias="thread" name="thread" primary="false" unique="true" type="BTREE">
    <column key="thread" length="" collation="A" null="false" />
  </index>
  <index alias="user" name="user" primary="false" unique="true" type="BTREE">
    <column key="user" length="" collation="A" null="false" />
  </index>
<aggregate alias="Thread" class="quipThread" local="thread" foreign="name" cardinality="one" owner="foreign" />
  <aggregate alias="Comments" class="quipComment" local="thread" foreign="thread" cardinality="many" owner="local" />
</object>
</model>

Вообще пакеты modx очень крутая штука. Сразу видно какие таблицы, зависимости между ними.   

Так для доступа к полям комментов: 

  1. Загружаем пакет quip. $modx->addPackage('quip',MODX_CORE_PATH.'components/quip/model/'); если ранее уже загружен сниппет quip - то этот шаг можно пропустить.
  2. получаем нужную запись из таблицы комментов по ID  -  $qu=$modx->getObject('quipComment', 30);   $body=$qu->get('body'); - и так можно выбрать любое поле
  3. Если по другим критериям то, например:
$c = $modx->newQuery('quipComment');
$c->prepare();  $total = $modx->getCount('quipComment',$c); - вернет кол-во строк всего и так далее.
Все варианты выборки можно глянуть тут http://mdx.ib35.ru/mdx/modx-snippets-php-xpdo.html#h_3.

И так для всех таблиц(в схеме - это "object") .

Ответы на вопрос


Поначалу для формы и вывода ответов я ипользовал параметр parent

$arrParam=array();
$arrParam['thread']=$thread;
$arrParam['parent']=$parent;

Но в этом случае вывод несколько неожиданный. Quip пытается вывести дерево. Помимо самих ответов - еще и родительские узлы с надписью "Отклоненные". Я не стал разбираться,  а просто переделал вывод и форму для ответов. Убрал параметр parent. Сделал thread="A-"+ id вопроса. Т.е. сделал каждый вопрос - отдельной темой. И все заработало как надо.

 

Ссылки


https://docs.modx.com/extras/revo/quip - официальная документация

http://api.modx.com/quip/ - документация по API

P. S.


Quip - это классика жанра. Все строго в парадигме MODX Revo. 

Ну для своего времени). Сейчас все делается несколько иначе. 

  • процессоры запускаются из контроллеров. реализованы не через классы, а просто через include скрипта. Тут своя иерархия контроллеров. И базовый тоже свой.
  • конроллеры тут отвечают за отображение в контексте web. 
  • нет возможности через параметры добавить свои поля. как в jotX.
  • все манипуляции с базой через xPDO
  • хуки есть только в процессоре create

В итоге получилось 3 блока(страницы).

  1. Страница "Задать вопрос". Тут только форма для вопроса.
  2. Страница списков вопросов. Можно выбрать категорию. При выборе вопроса - попадаем на страницу ответов на этот вопрос.
  3. Страница ответов на выбранный вопрос. Тут в заголовке - сам вопрос. Ниже - все ответы на этот вопрос. Далее - фома добавления нового ответа. 

Помимо этого на каждой странице - есть кнопка с сылкой на страницу добавления вопроса.


Комментарии 0






Разрешённые теги: <b><i><br>Добавить новый комментарий: