Сегодня я продолжаю рассказывать о разработке системы отслеживания ошибок. В этой части речь пойдет о создании списка багов и комментариев.
Напомню, в прошлый раз мы решили, что все для хранения записей о багах и комментариях к ним будем использовать две таблицы: bugs
и comments
.
Там же мы обсудили список полей и их назначение, и создали несколько внешних ключей для связи между таблицами.
Теперь переходим к созданию списка.
Чтобы лучше понять задачу рассмотрим html разметку, которую нам нужно получить.
<ul> <li> Описание бага №1 <ul> <li>Комментарий 1</li> <li> Комментарий 2 <ul> <li>Ответ на комментарий 2</li> </ul> </li> </ul> </li> <li> Описание бага №2 <ul> <li>Комментарий 1 к багу 2</li> ... </ul> </li> </ul>
Как видите, разметка достаточно простая. Мы создаем список, каждый элемент которого соответствует записи о баге. Если к этому багу оставлены комментарии, то они размещаются во вложенном списке. Вкладывая такие списки друг в друга, мы формируем дерево комментариев.
Таким образом, нам нужно преобразовать данные из таблиц в дерево.
Примечание. Если вы интересуетесь хранением древовидных структур в базе данных, то советую почитать статью «Построение деревьев».
Теперь посмотрим, каким образом можно сформировать такой список. Есть несколько вариантов.
Можно решать задачу «в лоб». Т.е. первым запросом найти все записи о багах (напомню, у всех таких записей поле parent_id = NULL
). А после этого в цикле отправлять запросы поиска комментариев к каждому из найденных багов. Недостаток такого подхода – большое количество запросов к БД.
Второй вариант. Мы можем получить всю необходимую информацию с помощью одного запроса. Но при этом усложняется алгоритм получения дерева с багами и комментариями.
Остановимся мы, конечно, на втором варианте.
И, прежде всего, нам нужно получить данные. Для этого создаем модель, назовем её mbug
(application\models\mbugs.php), и метод getAllBugsWithComments
.
class MBug extends Model {function MBug() { parent::Model(); } /** * Ищет все баги и комментарии к ним. Баги возвращаются начиная с последнего добавленного. * * @return массив со всеми багами и комментариями к ним, FALSE - если ничего не найдено */ function getAllBugsWithComments() { $qGetAll = '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 bugs AS b' .' LEFT JOIN categories AS c ON b.category_id=c.id' .' LEFT JOIN comments AS cm ON b.id=cm.bug_id' .' ORDER BY b.bug_date DESC'; $res = $this->db->query($qGetAll); if ($res->num_rows() <= 0) { return false; } return $res->result_array(); } }
Работает метод getAllBugsWithComments
предельно просто. Он выполняет запрос к БД и возвращает результат его выполнения в виде массива.
Самая интересная часть – это, конечно, сам запрос. Для того, чтобы сформировать дерево, нам нужны данные о багах, сведения о категориях к которым они относятся и комментарии к ним.
Поэтому в запросе мы выполняем объединение трех таблиц (bugs
, categories
, comments
). Рассмотрим, как выполняется это объединение.
Прежде всего, обратите внимание на то, что таблицы объединяются слева направо (LEFT JOIN
). На первом этапе объединяются таблицы bugs
и categories
. Т.к. каждому багу соответствует только одна категория, то в результате мы получим исходную таблицу bugs
с двумя новыми столбцами link
и name
из таблицы categories
.
Обратите внимание, что т.к. таблица bugs
расположена слева, то в результат войдут только те записи из таблицы categories
, для которых есть соответствующая запись в таблице bugs
. Другими словами, если у нас есть категория, в которой нет ни одного бага, то сведений о ней в результирующей таблице не будет.
На следующем этапе происходит объединение с таблицей comments. Тут возможны два варианта.
Первый – очередной баг не имеет комментариев. В этом случае в результирующей таблице будет одна строка со сведениями из таблиц bugs
и categories
, а все поля, соответствующие таблице comments
будут иметь значения NULL
.
Второй – очередной баг имеет один или более комментариев. Тогда в результирующей таблице будет одна или более строк с данными из таблиц bugs
, categories
и comments
. Причем количество таких строк определяется количеством комментариев.
Посмотрите на пример такой таблицы (я опустил часть полей).
id | title | … | name | … | c_description |
1 | Баг 1 | … | Критические ошибки | … | Комментарий к багу 1 |
2 | Баг 2 | … | Пожелания | … | NULL |
3 | Баг 3 | … | Критические ошибки | … | Первый комментарий к багу 3 |
3 | Баг 3 | … | Критические ошибки | … | Второй комментарий к багу 3 |
Переходим к преобразованию этой таблицы в HTML список.
Т.к. операция довольно сложная, то будет удобно написать для неё небольшую библиотеку – Table2Tree
.
Для этого создаём файл application\libraries\table2tree.php
(название файла совпадает с названием библиотеки).
Но, прежде чем переходить к описанию библиотеки, рассмотрим пример её использования. Допустим, у нас есть контроллер (bugtracker
) с методом page
(формирует страницу со списком багов и комментариев).
class BugTracker extends Controller { //настройки отображения списка багов private $listConf = array( 'commentOpen'=>'<li class="depth-{depth}">' ); function BugTracker() { parent::Controller(); $this->load->model('mbug'); $this->load->library('Table2Tree'); } function page($firstBug = 0) { ... //загружаем общий список ошибок и комментариев к ним $bugs = $this->mbug->getAllBugsWithComments(); if ($bugs !== false) { $bugsTree = $this->table2tree->getTree($bugs, $firstBug, $this->config->item('bugs_per_page')); $pageData['bugsList'] = $this->table2tree->getHTMLList($bugsTree, $this->listConf); } //формируем страницу } }
Как видите, в конструкторе мы загрузили нашу библиотеку (Table2Tree
) и модель (mbug
).
Метод page()
работает следующим образом.
1) Получаем список багов с комментариями (с помощью метода getAllBugsWithComments
модели).
2) Преобразовываем полученную таблицу в многомерный массив (метод getTree
нашей библиотеки).
3) Преобразовываем многомерный массив в HTML список (метод getHTMLList
).
На втором и третьем этапе я хочу остановиться подробнее. Основной вопрос тут: что представляет собой многомерный массив и зачем он вообще нужен.
Дело в том, что мы формируем этот массив таким образом, что его структура полностью совпадает со структурой HTML списка который мы хотим получить.
Например, для списка, показанного в начале статьи, этот массив будет выглядеть примерно так.
[0]=> ‘title’=>’Описание бага №1’ ... ‘comments’=> [0]=> ‘description’=>’Комментарий 1’ [1]=> ‘description’=>’Комментарий 2’ ‘comments’=> [0]=> ‘description’=>’Ответ на комментарий 2’ [1]=> ‘title’=>’Описание бага №2’ ... ‘comments’=> [0]=> ‘description’=>’Комментарий 1 к багу 2’
Используя такой массив, получить HTML список будет значительно проще.
Теперь рассмотрим, каким образом можно преобразовать результаты поиска по БД в такой массив.
Я решил разбить задачу на 2 этапа.
На первом – формируем массив с багами. В нем каждый элемент будет содержать описание бага и одномерный массив со всеми комментариями к нему.
На втором – преобразуем массивы с комментариями в многомерные (на основании значения в поле parent_id
), т.е. формируем дерево комментариев.
Рассмотрим метод getTree
function getTree($data, $firstBug = 0, $count = 1) { $sTree = $this->_convert2SimpleTree($data); //запоминаем общее количество багов $this->bugsCount = count($sTree); //оставляем в массиве только те элементы, которые будут отображаться на странице $sTree = array_slice($sTree, $firstBug, $count, TRUE); foreach ($sTree as $i => $row) { if (!empty($row['comments'])) { $sTree[$i]['comments'] = $this->_nestedComments($row['comments'], null, 0); } } return $sTree; }
Он имеет 3 входных параметра:
data
– массив с результатами поиска в БД;
firstBug
– номер первого бага на странице (используется для пагинации, нет никакого смысла строить дерево комментариев для багов, которые не будут отображаться на странице);
count
– количество багов на странице.
Первый этап работы (формирование массива с багами) осуществляется с помощью метода _convert2SimpleTree
(по соглашению, принятому в CodeIgniter, имена приватных методов должны начинаться с подчеркивания).
После этого мы оставляем в массиве только те записи о багах, которые будут отображаться на странице (строка 6).
И формируем дерево комментариев. Для этого мы проходим в цикле по всем элементам массива и с помощью метода _nestedComments
преобразовываем массив comments
в многомерный.
Рассмотрим метод _convert2SimpleTree
function _convert2SimpleTree($bugs) { $bugRes = array(); $currentBugId = 0; $i = -1; $j = 0; foreach ($bugs as $bug) { if ($bug['id'] != $currentBugId) { $currentBugId = $bug['id']; $i++; //заполняем массив данными о баге $bugRes[$i]['id'] = $bug['id']; $bugRes[$i]['title'] = $bug['title']; $bugRes[$i]['uname'] = $bug['uname']; $bugRes[$i]['category_id'] = $bug['category_id']; $bugRes[$i]['description'] = $bug['description']; $bugRes[$i]['status'] = $bug['status']; $bugRes[$i]['bug_date'] = $bug['bug_date']; $bugRes[$i]['uemail'] = $bug['uemail']; //данные о категории $bugRes[$i]['link'] = $bug['link']; $bugRes[$i]['category'] = $bug['category']; //создаем массив с комментариями if ($bug['c_id'] != NULL) { $bugRes[$i]['comments'] = array(); $j = 0; $bugRes[$i]['comments'][$j]['id'] = $bug['c_id']; $bugRes[$i]['comments'][$j]['uname'] = $bug['c_uname']; $bugRes[$i]['comments'][$j]['description'] = $bug['c_description']; $bugRes[$i]['comments'][$j]['comment_date'] = $bug['comment_date']; $bugRes[$i]['comments'][$j]['parent_id'] = $bug['parent_id']; $bugRes[$i]['comments'][$j]['bug_id'] = $bug['bug_id']; $bugRes[$i]['comments'][$j]['uemail'] = $bug['c_uemail']; $j++; } } else { //добавляем новую запись в массив с комментариями $bugRes[$i]['comments'][$j]['id'] = $bug['c_id']; $bugRes[$i]['comments'][$j]['uname'] = $bug['c_uname']; $bugRes[$i]['comments'][$j]['description'] = $bug['c_description']; $bugRes[$i]['comments'][$j]['comment_date'] = $bug['comment_date']; $bugRes[$i]['comments'][$j]['parent_id'] = $bug['parent_id']; $bugRes[$i]['comments'][$j]['bug_id'] = $bug['bug_id']; $bugRes[$i]['comments'][$j]['uemail'] = $bug['c_uemail']; $j++; } } return $bugRes; }
Выглядит он довольно объемным, но большую часть его составляют операторы присвоения. А принцип работы достаточно простой.
В исходной таблице (с результатами из БД) каждому багу может соответствовать несколько строк (в зависимости от количества комментариев).
Поэтому мы перебираем в цикле все строки этой таблицы и если встречаем новый баг (его id
отличается от предыдущего), то создаём новый элемент в результирующем массиве и копируем в него данные бага, категории и комментария (если он есть).
Если очередная строка относится к той же записи о баге, что и предыдущая, то мы копируем только данные комментария.
Продолжение на следующей странице >>
Страница: 1 2