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

Владимир | | CodeIgniter, HTML, PHP, Web разработка.

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

Напомню, в прошлый раз мы решили, что все для хранения записей о багах и комментариях к ним будем использовать две таблицы: 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