Bug tracker. Преобразуем таблицу в html список (часть 4).

9 марта, 2009

Начало на предыдущей странице

Теперь рассмотрим метод _nestedComments.

  1. function _nestedComments($comments, $parentId, $depth) {
  2.     $commentsRes = array();
  3.     $i = 0;
  4.     foreach ($comments as $val) {
  5.         if ($val['parent_id'] == $parentId) {
  6.             $commentsRes[$i] = $val;
  7.             $commentsRes[$i]['depth'] = $depth;
  8.             $commentsRes[$i]['comments'] = $this->_nestedComments($comments, $val['id'], $depth + 1);
  9.             $i++;
  10.         }
  11.     }
  12.     return $commentsRes;
  13. }

Он значительно меньше предыдущего, но принцип его работы сложнее. Прежде всего, обратите внимание на входные параметры:
comments – массив с комментариями отдельного бага;
parentIdid родительского комментария;
depth – глубина рекурсии.

Думаю, вы уже догадались, что метод рекурсивный, т.е. вызывает сам себя. Причем первый параметр остаётся неизменным, а второй и третий изменяются.

При первом вызове parentId = NULL (соответствует комментарию верхнего уровня). Мы в цикле перебираем все комментарии и ищем те, у которых элемент parent_id равен текущему значению parentId.

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

Для этого вызываем этот же метод (_nestedComments), но уже во втором параметре указываем id текущего комментария.

В результате мы получим дерево комментариев. Глубина рекурсии будет совпадать с уровнем вложенности комментариев.

Переходим к формированию HTML списка.

Рассмотрим метод getHTMLList.

  1. function getHTMLList($tree, $config = null) {
  2.     if (!empty($config)) {
  3.         foreach ($config as $key => $value) {
  4.             $this->config[$key] = $value;
  5.         }
  6.     }
  7.     $this->CI->load->library('parser');
  8.     $html = $this->config['listOpen']."\n";
  9.     foreach ($tree as $node) {
  10.         $html .= $this->config['bugOpen'].$this->CI->parser->parse('bug_tpl', $node, TRUE)."\n";
  11.         if (!empty($node['comments'])) {
  12.             $html .= $this->_convert2HTMLList($node['comments'], 0)."\n";
  13.         }
  14.         $html .= $this->config['bugClose'];
  15.     }
  16.     return $html.$this->config['listClose'];
  17. }

Его первый параметр – массив с багами и комментариями, который мы получили до этого.

Второй – массив с настройками отображения списка. Он позволяет легко изменять HTML разметку, которую формирует этот метод.
По-умолчанию, поля этого массива имеют следующие значения

  1. private $config = array(
  2.     'listOpen' => '<ul>'
  3.     ,'listClose' => '</ul>'
  4.     ,'bugOpen' => '<li>'
  5.     ,'bugClose' => '</li>'
  6.     ,'commentOpen' => '<li>'
  7.     ,'commentClose' => '</li>'
  8. );

Т.е. будет сформирован ненумерованный список. Изменив значения listOpen и listClose на <ol> и </ol> мы получим нумерованный список.

Кроме того, в результирующий список данные о багах должны быть вставлены с соответствующей разметкой. Поэтому, чтобы не превращать код метода в мешанину php и html, мы используем встроенный в CodeIgniter шаблонизатор.

Сами шаблоны мы рассмотрим чуть ниже, а сейчас достаточно знать, что с помощью
$this->CI->parser->parse(...)
можно получить соответствующую html разметку.

Теперь взгляните на цикл (строки 9-15). В нём мы перебираем все элементы массива и для каждого вызываем метод _convert2HTMLList.

  1. function _convert2HTMLList($bugComments, $depth) {
  2.     $list = ";
  3.     $list .= $this->config['listOpen']."\n";
  4.     foreach ($bugComments as $comment) {
  5.         $openTag = str_replace('{depth}', $depth, $this->config['commentOpen']);
  6.         $list .= $openTag.$this->CI->parser->parse('comment_tpl', $comment, TRUE)."\n";
  7.         if (!empty($comment['comments'])) {
  8.             $list .= $this->_convert2HTMLList($comment['comments'], $depth + 1);
  9.         }
  10.         $list .= $this->config['commentClose']."\n";
  11.     }
  12.     $list .= $this->config['listClose'];
  13.     return $list;
  14. }

Думаю, вы заметили, что этот метод тоже рекурсивный. Мы перебираем все элементы массива комментариев и с помощью шаблонизатора формируем HTML разметку.

Если текущий элемент массива имеет вложенный массив с именем comments (т.е. есть дочерние комментарии), то мы вызываем этот же метод, но в качестве первого параметра передаем ему вложенный массив.

Второй параметр метода содержит значение глубины рекурсии.

Посмотрите на HTML разметку в начале этой статьи. Каждый вызов метода _convert2HTMLList формирует свой тег <ul> для комментариев. Так для первого бага этот метод будет вызван 2 раза. Первый раз из метода getHTMLList при создании списка с комментариями нулевого уровня (комментарий 1, комментрарий 2), а второй раз – рекурсивно при создании списка с ответами на комментарий 2.

В результате мы получим необходимую разметку со вложенными HTML списками.

Теперь посмотрите на шаблоны для багов и комментариев. Они должны быть размещены в папке application\views.

bug_tpl.php

  1. <div class="bug" id="bug-{id}">
  2.     <p class="bugTitle">Название: <?php echo anchor('bugtracker/bug/'.'{id}', '{title}'); ?></p>
  3.     <p class="bugInfo">
  4.         <span class="bugAuthor">Автор: {uname}.</span>
  5.         <span class="bugCategory">Категория: <?php echo anchor('bugtracker/category/'.'{link}', '{category}'); ?>.</span>
  6.         <span class="bugDate">Добавлен: {bug_date}.</span>
  7.     </p>
  8.     <p class="bugDescription">Описание: {description}</p>
  9. </div>

comment_tpl.php

  1. <div class="comment depth-{depth}" id="comment-{id}">
  2.     <p class="commentInfo">
  3.         <span class="commentAuthor">Автор: {uname}.</span>
  4.         <span class="commentDate">Дата: {comment_date}.</span>
  5.         <span class="commentDescription">Ответ: {description}</span>
  6.     </p>
  7. </div>

Обратите внимание на параметры в фигурных скобках. Их имена совпадают с ключами полей в массиве с деревом багов и комментариев. При формировании страницы шаблонизатор CodeIgniter заменит их соответствующими значениями.

На этом я закончу эту часть. В следующий раз мы рассмотрим создание записи о новом баге.

До встречи!

P.S. Как всегда я буду рад услышать любые замечания и комментарии (кроме спамерских ;) ).

Страницы: 1 2

Понравилась статья? Подписывайтесь на продолжение rss link !

Или на мой твиттер twitter link

]]>

Добавьте эту страницу в google.com bobrdobr.ru del.icio.us technorati.com linkstore.ru news2.ru rumarkz.ru memori.ru moemesto.ru

]]>

Опубликовано в CodeIgniter, HTML, PHP, Web разработка Комментарии (18) »

]]>

Комментарии (18)

Вы можете отслеживать обсуждение записи с помощью RSS 2.0 rss link

Вы также можете оставить комментарий, или трекбек с Вашего сайта.

]]>
  1. Big_Shark

    Что та очень сложно как то получилось.
    Выбирать все 1 запросом не удобно так как я считаю что для 1 таблицы БД должна быть 1 модель.
    Вызывать комментарии мы будем лишь на просмотре полной версии текста про баг так что думаю Проблем со скоростью при разбиении запросов тоже не должно возникнуть.
    Смешивать все баги и комменты в 1 массив тоже достаточно не удобно мне показалось я бы сделал 2 массива $bug и $comment
    и по $bug['id'] вытаскивал бы комменты типа так $comment[$bug['id']].
    Очень ваш стильнаписания разница с моим так что будет интересно посмотреть на то что из этого выйдет.

  2. Peter

    Проще было бы получать отдельными запросами списки комментариев для каждого бага и сразу же вызывать для каждого из них $this->CI->parser->parse, не занося их в промежуточный многомерный массив.

    Чтобы построить дерево комментариев, можно извлекать из базы комментарии, являющиеся потомками текущего комментария, как это сделано в статье http://www.sitepoint.com/hierarchical-data-database/ (см. функцию display_children)

  3. Peter

    Извините за "битую" ссылку. Вот правильная: http://www.sitepoint.com/print/hierarchical-data-database/

  4. Польза сомнительно. Притом трудоёмко.

  5. Я так понял, ты выбираешь из базы все дерево (все баги и все комменты, к ним привязанные), а уже на этапе построения дерева отрезаешь slice`ом то, что нужно для пейджера… А если база будет очень большая? Зачем нам столько лишних данных?

    Логичнее было бы реализовывать по страничное отображение через введение лимита в запрос на базу данных, не вытягивая при этом ни лишних багов, ни лишних комментариев.

    По своему опыту скажу, что лучше вытягивать сначала список по основной таблице, а уже потом по зависящей. Т.е. сначала тянем баги, потом по каждый баг комментарии. Такое функциональное разделение может потом сильно пригодиться. Например, если тебе нужно будет переформатировать список (сделать выборку по определенным условиям). Или скажем такая задача – организовать рсс ленту новых багов (просто их заголовков). Твое решение слишком "неповоротливо" для таких целей.

    В общем, дело стиля.. Я на ZendFW работаю, может в CI действительно удобнее по-другому.

  6. Сразу поясню, почему я использовал один запрос. Идея в том, что на страницах баг трекера всегда должны отображаться и баги и комментарии. После загрузки страницы все комментарии будут автоматически сворачиваться (с помощью JS) и посетитель увидит только заголовки. При клике по заголовку список комментариев будет развернут.
    Это должно увеличить скорость работы со страницей, т.к. подгружать комментарии с помощью отдельных запросов не придется.

    Разбить на 2 массива таблицу можно, но не уверен, что такой вариант был бы намного проще. В этом случае пришлось бы:
    1) для каждого бага найти соответствующие ему комментарии во втором массиве;
    2) с помощью рекурсивной функции построить деревья для каждой группы комментариев.

    Вариант, который приведен в статье на sitepoint.com, конечно, выглядит гораздо красивее и короче. Но при этом возникает ситуация, которой я хотел избежать. Автор той статьи предлагает выполнять запросы внутри рекурсивной функции, а это означает, что если у нас есть десять вложенных комментариев, то для их получения будет выполнено 10 запросов. Т.е. может получиться, что мы будем "тянуть" из базы комментарии по одному.

    Не думаю, что между ZendFW и CI в этом плане есть разница. Во всяком случае CI не накладывает своих ограничений на работу с БД. И я конечно не предлагаю использовать такой запрос для страниц на которых не нужны комментарии. Этот пример касается только страницы с общим списком. Для формирования RSS ленты или страницы с отдельным багом я добавлю дополнительный метод в модель, который будет получать, например, только заголовки багов из БД.

    >> Зачем нам столько лишних данных?

    Вот тут вы абсолютно правы. Это основной недостаток данного подхода. Проблема в том, что использовать LIMIT не получается, т.к. количество записей определяется числом комментариев, а не багов. А разбивка на страницы осуществляется именно по количеству багов. По-моему, должен быть какой-то способ решить эту проблему.

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

    Хотя, может быть я тут не прав. Нужно потестировать скорость выполнения этих вариантов.

  7. Владимир, вот это очень сомнительно:

    >>После загрузки страницы все комментарии будут автоматически >>сворачиваться (с помощью JS) и посетитель увидит только >>заголовки. При клике по заголовку список комментариев будет >>развернут.
    >>Это должно увеличить скорость работы со страницей, т.к. >>подгружать комментарии с помощью отдельных запросов не >>придется.

    Я когда-то работал с поисковой системой, которая отображала данные в стиле: "Тайтл + при клике на тайтл – дополнительные данные". Я тоже сначала сделал вывод всего, а потом "схлопывание" всего кроме тайтлов. Страничка грузиться реально долго при этом, потому что объемы текстовых данных большие (хоть они и не видны).

    Эту проблему и проблему с пейджингом можно решить с помощью AJAX – показывает названия багов (и описание по желанию) с постраничной разбивкой. Под этим пишем "Комментарии (Н штук)". При клике на ссылку "Комментарии" посылаем AJAX запрос, получаем список комментариев (дерево), отображаем его и ВНУТРЕННИЙ ПЕЙДЖЕР для комментариев, если их сильно много. При чем этот пейджер работает также на AJAXе.

    Т.е. у багов свое постраничное отображение, у комментов свое.

    Такой подход решает все твои проблемы (и лишнии данные не дергаем, и пользователь не ждет загрузки километровых лент комментов). Еще один плюс к данной моделе – можно легко организовывать кеширование блоков комментариев (если у нас все дерево на руках, то кеширование практически невозможно).

    • >> это очень сомнительно

      Хотел привести несколько доводов в защиту своего варианта, но, похоже, вы правы. Я думал только о текстовом варианте страницы, она грузится быстро. Но как только в комментариях появятся картинки о быстрой загрузке можно будет забыть.

      Кстати, я нашел способ получить все данные одним запросом, но с ограничением по количеству комментариев.

      SELECT b. * , c.link, c.name AS category, cm.id AS c_id,
      cm.uname AS c_uname, cm.description AS c_description,
      cm.comment_date, cm.parent_id, cm.bug_id, cm.uemail AS c_uemail
      	FROM comments AS cm
      	RIGHT JOIN (
      	SELECT * FROM bugs
      	LIMIT 0 , 2
      	) b ON b.id = cm.bug_id
      	LEFT JOIN categories AS c ON b.category_id = c.id
      	ORDER BY b.bug_date DESC
  8. Big_Shark

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

    Каким запросом в цикле вы о чем?
    Все делается очень просто получаем список ID багов и после этого делаем запрос к таблице с комментарием примерное так
    $this->db->where_in('bug_id', $array_bug_id);
    После чего получившейся массив уже рекурсивно превращаем в дерево.
    Лучше подгружать комментарии ajaxом и кэшировать полученное дерево через сразу показывать все комментарии на страницы и скрывать их так как размер страницы увелчиваеться очень сильно.

    • >> $this->db->where_in('bug_id', $array_bug_id);

      Согласен, так значительно лучше ;) Но я написал "потом отдельно комментариев для каждого бага". А вашим запросом будут вытянуты сразу все комментарии.

      В общем, я провел несколько экспериментов, и похоже проблема решается немного проще ;) Все зависит от того, какие данные на какой странице нужно показывать. И если добавить несколько соответствующих методов в модель, то искать компромиссные варианты не придется.
      Подробно напишу в следующем посту.

      • Big_Shark

        А вашим запросом будут вытянуты сразу все комментарии.

        Все комментарии для списка багов.

        Интересно уже прочитать следующею статью)

  9. Peter

    В упомянутой статье приводится ещё один алгоритм (Modified Preorder Tree Traversal Algorithm). С его помощью можно получить иерархический список комментариев одним запросом (не нужно строить дерево в PHP-коде). Если в багтрекере будут глубоко вложенные деревья комментариев, то этот метод должен работать быстрее, чем adjacency list, который Вы используете.

    Другая статья о том же алгоритме: http://dev.mysql.com/tech-resources/articles/hierarchical-data.html (здесь решение на чистом SQL, без PHP).

    Имхо, лучшее решение — извлекать список комментариев отдельным запросом для каждого бага и использовать preorder tree traversal для вывода комментов. Тогда алгоритм будет оптимальным, и при необходимости можно будет легко перейти к AJAX, как предлагает Алексей.

    P.S. Спасибо за интересный блог.

    • В комментариях к прошлой части немного обсуждали эту тему (использование Nested Set Model). Мое мнение – это довольно сложный вариант. Приводить код без пояснений я не хочу, а если начать рассказывать о Nested Set, то вместо цикла о разработке баг трекера, получится цикл о принципах работы с Nested Set :)

  10. используем метод описанный петером. работает.

  11. Nested Set Model – в принципе, если вникнуть, то очень удобно

  12. Занимательно, спасибо.

]]>

Оставить комментарий

* - обязательные для заполнения поля

]]>