Bug Tracker: установка фреймворка и создание базы данных (часть вторая)

Владимир | | Ajax, CodeIgniter, htaccess, JavaScript, MySQL, PHP, Web разработка.

bug_tracker_logo_part2

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

Но прежде чем переходить к основной теме, хочу поблагодарить всех комментаторов и особенно AmdY, Big_Shark и Алексея Качаева за советы!

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

В прошлый раз с инструментами мы определились (используем PHP, фреймворк CodeIgniter, MySQL и jQuery).

Сейчас нам нужно их установить и настроить.

Я предполагаю, что web сервер, PHP и MySQL у вас уже настроены, поэтому нам нужно только скачать CodeIgniter и jQuery. После этого распаковываем архивы в какую-нибудь папку на сервере. В результате должна получиться примерно следующая структура папок.

index.php
.htaccess
system/
js/
	jquery-1.3.2.min.js
css/

Файл index.php и папка system – из дистрибутива CodeIgniter.

.htaccess – самый обычный. Сразу приведу его содержимое.

<IfModule mod_rewrite.c>
	RewriteEngine On
	RewriteBase /

	RewriteCond %{REQUEST_URI} ^system.*
	RewriteRule ^(.*)$ /index.php?/$1 [L]

	RewriteCond %{REQUEST_FILENAME} !-f
	RewriteCond %{REQUEST_FILENAME} !-d
	RewriteCond $1 !^(index\.php|images|robots\.txt|css|js)
	RewriteRule ^(.*)$ index.php?/$1 [L]
</IfModule>

<IfModule !mod_rewrite.c>
	ErrorDocument 404 /index.php
</IfModule>

Основное его назначение – автоматически подставлять index.php в ссылки. Т.е. ссылка вида

bugtracker.local/bugtracker/page/2

будет преобразована в

bugtracker.local/index.php/bugtracker/page/2

При этом преобразование происходит, только если указанного в URL файла на самом деле не существует. Т.е. если запрос будет к js или css файлу, то index.php подставляться не будет.

Кстати, если вы хотите, чтобы приложение находилось в какой-нибудь папке (например, mysite.domen/bugtracker/), то нужно будет изменить параметр RewriteBase.

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

Все конфигурационные файлы находятся в папке system/application/.

autoload.php – загружаем библиотеку для работы с базой данных и URL хелпер

$autoload['libraries'] = array('database');
$autoload['helper'] = array('url');

config.php – здесь указываем адрес приложения, например,

$config['base_url'] = "http://bugtracker.local/";

и добавляем ещё один параметр

$config['bugs_per_page'] = 5;

Как несложно догадаться его значение определяет количество записей о багах на странице.

routes.php – тут нужно указать только название контроллера, который будет использоваться по-умолчанию. Я решил его называть bugtracker.

$route['default_controller'] = "bugtracker";

database.php – здесь заполняем массив с параметрами подключения к базе данных.

Примечание. Подробнее о настройке фреймворка можно почитать в статье «PHP framework CodeIgniter. Установка и настройка.»

Теперь переходим к созданию базы данных.

Нам потребуются три таблицы:

1) bugs – используется для хранения информации о найденных багах и комментариях к ним.

Поля:
id – первичный ключ;
title – заголовок;
uname – имя пользователя, который оставил сообщение;
categoryid – id категрии к которой относится баг (должно совпадать с соответствующим полем в таблице categories);
description – описание бага;
status – здесь будет храниться информация о состоянии бага (по-умолчанию, «не исправлено»);
replyto – это поле устанавливает связь между багами и комментариями к ним (подробнее на этом мы остановимся чуть позже).

2) categories – список категорий.

Поля:
id – первичный ключ;
link – ссылка на категорию (латинскими буквами);
name – названии категории.

3) users – информация о пользователях. В данном случае этими пользователями будут только администраторы баг трекера. Оставлять сообщения и комментарии сможет кто угодно без авторизации.

id – первичный ключ;
name – имя пользователя;
pass – пароль.

Теперь посмотрите на запросы, которые создают эти таблицы.

CREATE TABLE `bugs` (
	`id` int(11) NOT NULL auto_increment,
	`title` varchar(300) NOT NULL,
	`uname` varchar(100) NOT NULL,
	`categoryid` int(11) default NULL,
	`description` text NOT NULL,
	`status` varchar(100) NOT NULL default 'не исправлено',
	`replyto` int(11) default NULL,
	PRIMARY KEY  (`id`),
	KEY `replyto` (`replyto`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

ALTER TABLE `bugs`
	ADD CONSTRAINT `bugs_ibfk_1` FOREIGN KEY (`replyto`) REFERENCES `bugs` (`id`) ON DELETE CASCADE;

CREATE TABLE `categories` (
	`id` int(11) NOT NULL auto_increment,
	`link` varchar(100) NOT NULL,
	`name` varchar(300) NOT NULL,
	PRIMARY KEY  (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

CREATE TABLE `users` (
	`id` int(11) NOT NULL auto_increment,
	`name` varchar(100) NOT NULL,
	`pass` varchar(100) NOT NULL,
	PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

Самый интересный момент здесь касается таблицы bugs, а точнее её поля replyto. Оно даёт возможность отличить комментарий к багу от самого бага. Если значение в нём не равно NULL, то это комментарий, если равно – баг. При этом число, записанное в поле replyto, указывает, к какому багу относится данный комментарий.

Например, в таблице есть четыре следующий записи:

id title replyto
1 Bug 1 NULL
2 Comment 1 1
3 Comment 2 1
4 Bug 2 NULL

Это означает, что мы имеем два бага (записи 1 и 4) и два комментария (записи 2 и 3), которые относятся к первому багу.

Кстати, значение поля categoryid для всех комментариев равно NULL.

Тут возникает проблема. Что будет с комментариями, если мы удалим баг к которому они относятся? Можно, конечно, решить эту проблему так:

1) найти все комментарии, которые относятся к данному багу;
2) удалить их;
3) удалить сам баг.

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

Для этого мы объявляем поле replyto внешним ключом (т.е. связываем его с полем id) и указываем опцию ON DELETE CASCADE (каскадное удаление) (строки 13-15). Теперь при удалении записи у которой поле id = 3 движок БД будет искать в этой же таблице все записи у которых replyto = 3 и удалять их.

И, кроме того, это поле (replyto) мы будем использовать при выводе сообщений о багах на страницах нашего баг трекера.

Но об этом в следующий раз.

До встречи!

Интересно почитать:
Слушать радио онлайн

  • Big_Shark

    Мне кажется было бы удобнее делать баги в 1 таблицы а комментарии в другой
    так как у комментариев не будет статуса так далее.

  • Big_Shark

    Мне кажется было бы удобнее делать баги в 1 таблицы а комментарии в другой
    так как у комментариев не будет статуса так далее.

  • SM

    Можно сказать классика
    дб багтреков

    В твоем случае, всетаки яб разделил комменты и сами баги, Big_Shark +1

    Так как захочется добавить файло к багам — табличку еще одну создал и вперед 🙂 так сказать «с перспективой на будущее»

  • SM

    Можно сказать классика
    дб багтреков

    В твоем случае, всетаки яб разделил комменты и сами баги, Big_Shark +1

    Так как захочется добавить файло к багам — табличку еще одну создал и вперед 🙂 так сказать «с перспективой на будущее»

  • SM

    хм, сранно както линк сработал, повторяю

  • SM

    хм, сранно както линк сработал, повторяю

  • Честно говоря, я и сам думал разделить таблицы. Но потом возникла идея сделать комментарии с любым уровнем вложенности, т.е. в поле replyto может быть не только ссылка на баг, но и на другой комментарий. Даже написал рекурсивную функцию, которая преобразовывала такую таблицу в структурированный список.

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

    В общем, решил сделать один уровень вложенности, а таблица осталась в неизменном виде.

    Пример zend'а. У них немного другие задачи 🙂 Например, таблица bugs используется для хранения информации о багах в разных продуктах. А таблицы с комментариями вообще нет.

    С файлами я планировал сделать немного проще. После загрузки файла — вставлять ссылку на него в текст описания бага…

    Пишу я этот комментарий и понимаю, что структура БД у меня явно недодуманная. И, самое главное, я не определился четко с перечнем функций. И начинаются мысли, то ли так сделать, то ли по-другому 🙂

  • Честно говоря, я и сам думал разделить таблицы. Но потом возникла идея сделать комментарии с любым уровнем вложенности, т.е. в поле replyto может быть не только ссылка на баг, но и на другой комментарий. Даже написал рекурсивную функцию, которая преобразовывала такую таблицу в структурированный список.

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

    В общем, решил сделать один уровень вложенности, а таблица осталась в неизменном виде.

    Пример zend'а. У них немного другие задачи 🙂 Например, таблица bugs используется для хранения информации о багах в разных продуктах. А таблицы с комментариями вообще нет.

    С файлами я планировал сделать немного проще. После загрузки файла — вставлять ссылку на него в текст описания бага…

    Пишу я этот комментарий и понимаю, что структура БД у меня явно недодуманная. И, самое главное, я не определился четко с перечнем функций. И начинаются мысли, то ли так сделать, то ли по-другому 🙂

  • Не понимаю следующих вещей в .htaccess:

    RewriteCond %{REQUEST_URI} ^system.*
    RewriteRule ^(.*)$ /index.php?/$1 [L]

    для чего это?

    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond $1 !^(index.php|images|robots.txt|css|js)

    Если мы уже проверили существует ли файл или директория, то зачем выполнять третье условие?

    К примеру, мой список условий:

    RewriteCond $1 !^(index.php)
    RewriteCond %{REQUEST_URI} !.(css|js|jpg|gif|png)$
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d

    Если не запрашивается index.php,
    если не запращиваются файлы с окончаниями css|js|jpg|gif|png,
    только после этого еще раз дополнительно проверяем наличие запрашиваемого файла, диреткории на реальное существование (то есть будет происходить обращение к файловой системе, до этого все исходные данные брались из адресной строки)

    Если какое-либо из условий возращает false, то дальнейшая цепочка условий прекращается.

    Пожалуйста, поправьте меня, если я не прав. Если ваше решение лучше, объясните, пожалуйста, почему и что к чему.

    • Первый фрагмент

      RewriteCond %{REQUEST_URI} ^system.*
      RewriteRule ^(.*)$ /index.php?/$1 [L]

      закрывает доступ в папку system для посетителей (только через index.php).

      А вот насчет второго фрагмента вы правы. Третья строка вообще лишняя. У меня все прекрасно работает и без неё. Но я видел несколько сообщений на форумах о том, что без этой строки были проблемы с доступом к статическим файлам. Скорее всего, у них были ещё какие-то правила, которые влияли на результат, но я решил не рисковать и поэтому привел файл из codeigniter wiki без каких-либо изменений.

      • На сколько я знаю, тут еще важен порядок, то есть в моем случае, веб-сервер, если первое условие возращает false, в дальнейшем не обращается к файловой системе за проверкой на существование файлов/директорий. Могу ошибаться, поправьте, если не прав.

  • Не понимаю следующих вещей в .htaccess:

    RewriteCond %{REQUEST_URI} ^system.*
    RewriteRule ^(.*)$ /index.php?/$1 [L]

    для чего это?

    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond $1 !^(index\.php|images|robots\.txt|css|js)

    Если мы уже проверили существует ли файл или директория, то зачем выполнять третье условие?

    К примеру, мой список условий:

    RewriteCond $1 !^(index\.php)
    RewriteCond %{REQUEST_URI} !\.(css|js|jpg|gif|png)$
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d

    Если не запрашивается index.php,
    если не запращиваются файлы с окончаниями css|js|jpg|gif|png,
    только после этого еще раз дополнительно проверяем наличие запрашиваемого файла, диреткории на реальное существование (то есть будет происходить обращение к файловой системе, до этого все исходные данные брались из адресной строки)

    Если какое-либо из условий возращает false, то дальнейшая цепочка условий прекращается.

    Пожалуйста, поправьте меня, если я не прав. Если ваше решение лучше, объясните, пожалуйста, почему и что к чему.

    • Первый фрагмент

      RewriteCond %{REQUEST_URI} ^system.*
      RewriteRule ^(.*)$ /index.php?/$1 [L]

      закрывает доступ в папку system для посетителей (только через index.php).

      А вот насчет второго фрагмента вы правы. Третья строка вообще лишняя. У меня все прекрасно работает и без неё. Но я видел несколько сообщений на форумах о том, что без этой строки были проблемы с доступом к статическим файлам. Скорее всего, у них были ещё какие-то правила, которые влияли на результат, но я решил не рисковать и поэтому привел файл из codeigniter wiki без каких-либо изменений.

      • На сколько я знаю, тут еще важен порядок, то есть в моем случае, веб-сервер, если первое условие возращает false, в дальнейшем не обращается к файловой системе за проверкой на существование файлов/директорий. Могу ошибаться, поправьте, если не прав.

  • Big_Shark

    По мне так
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    Вообще лишнее
    так как как мы открывает доступ к определенным папкам
    RewriteCond $1 !^(index.php|images|robots.txt|css|js)
    А значит следовательно дальше этих папок не куда вылезти пользователь не должен

    • Big_Shark, а как на счет, к примеру, ссылок на файлы для скачивания?

      • Big_Shark

        RewriteCond $1 !^(index.php|images|robots.txt|css|js|fales)
        Дописываем папку для файлов и пускай себе качает на здоровья с этой папки.

        • Понял, спасибо, так будет лучше — без обращения к файловой системе )

        • Почему-то раньше я не обращал на этот нюанс внимание. Действительно, если есть возможность убрать обращение к файловой системе, то это нужно сделать.

          Еще один момент насчет ограничения доступа. Можно вынести папку system за пределы DOCUMENT_ROOT (например, на уровень выше), тогда в корне сайта останутся папки со статическими файлами и index.php. Т.е. всё к чему доступ разрешен. И можно будет вообще убрать
          RewriteCond %{REQUEST_URI} ^system.*
          RewriteRule ^(.*)$ /index.php?/$1 [L]

  • Big_Shark

    По мне так
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    Вообще лишнее
    так как как мы открывает доступ к определенным папкам
    RewriteCond $1 !^(index\.php|images|robots\.txt|css|js)
    А значит следовательно дальше этих папок не куда вылезти пользователь не должен

    • Big_Shark, а как на счет, к примеру, ссылок на файлы для скачивания?

      • Big_Shark

        RewriteCond $1 !^(index\.php|images|robots\.txt|css|js|fales)
        Дописываем папку для файлов и пускай себе качает на здоровья с этой папки.

        • Понял, спасибо, так будет лучше — без обращения к файловой системе )

        • Почему-то раньше я не обращал на этот нюанс внимание. Действительно, если есть возможность убрать обращение к файловой системе, то это нужно сделать.

          Еще один момент насчет ограничения доступа. Можно вынести папку system за пределы DOCUMENT_ROOT (например, на уровень выше), тогда в корне сайта останутся папки со статическими файлами и index.php. Т.е. всё к чему доступ разрешен. И можно будет вообще убрать
          RewriteCond %{REQUEST_URI} ^system.*
          RewriteRule ^(.*)$ /index.php?/$1 [L]

  • Big_Shark

    Тоже хороший вариант)

  • Big_Shark

    Тоже хороший вариант)

  • Max Sharof

    У меня вопрос. Вы в дампе указали движок InnoDB. Мой провайдер позволяет только MyISAM использовать. Будут ли какие то проблемы если я буду MyISAM юзать?? Спасибо.

    • SM

      Будут. Большие.

    • Да, проблемы будут. И, к сожалению, SM прав, большие.

      MyISAM не поддерживает каскадное удаление данных. В багтрекере при удалении записи о баге автоматически (движком MySQL) удалятся все связанные с ней комментарии. Т.к. при создании таблицы багов мы указали внешний ключ в таблице комментариев и указали опцию ON DELETE CASCADE. В случае с MyISAM вам придется удалять эти комментарии вручную, т.е. отдельными запросами.

      • michail1982

        В MyISAM можно сделать нечто подобное на триггерах after_update, типа грохнуть все коменты к багу, которого нет в таблице багов … чёт намудрил :))
        я имелл ввиду такое
        DELETE * from bugs where replyto not in (select bug_id from bugs where replyto is NULL)

        • Что-то не пойму как этот запрос работает

          select bug_id from bugs where replyto is NULL

          выберет все записи первого уровня (т.е. те, которые не являются комментариями)

          DELETE * from bugs where replyto not in…

          удалит все остальное, т.е. все комментарии, а нужно удалить один баг (первый уровень) и все комментарии к нему (всех уровней вложенности).

  • Max Sharof

    У меня вопрос. Вы в дампе указали движок InnoDB. Мой провайдер позволяет только MyISAM использовать. Будут ли какие то проблемы если я буду MyISAM юзать?? Спасибо.

    • SM

      Будут. Большие.

    • Да, проблемы будут. И, к сожалению, SM прав, большие.

      MyISAM не поддерживает каскадное удаление данных. В багтрекере при удалении записи о баге автоматически (движком MySQL) удалятся все связанные с ней комментарии. Т.к. при создании таблицы багов мы указали внешний ключ в таблице комментариев и указали опцию ON DELETE CASCADE. В случае с MyISAM вам придется удалять эти комментарии вручную, т.е. отдельными запросами.

      • michail1982

        В MyISAM можно сделать нечто подобное на триггерах after_update, типа грохнуть все коменты к багу, которого нет в таблице багов … чёт намудрил :))
        я имелл ввиду такое
        DELETE * from bugs where replyto not in (select bug_id from bugs where replyto is NULL)

        • Что-то не пойму как этот запрос работает

          select bug_id from bugs where replyto is NULL

          выберет все записи первого уровня (т.е. те, которые не являются комментариями)

          DELETE * from bugs where replyto not in…

          удалит все остальное, т.е. все комментарии, а нужно удалить один баг (первый уровень) и все комментарии к нему (всех уровней вложенности).

  • michail1982

    Да, всё верно, после удаления бага его ИД не будет в подзапросе (select bug_id from bugs where replyto is NULL),
    соответственно все комментарии на него удаляться

    • Удаляться не только все комментарии к данному багу, но и ко всем остальным. Т.е. ВООБЩЕ ВСЕ комментарии. Останутся только записи о самих багах.
      У каждого комментария в поле replyto записано какое-то значение, либо id бага, либо id другого комментария (для комментариев с уровнем вложенности 2 и >).

      • michail1982

        это всего-лишь MyISAM………..
        при желании можно и чтото путёвое написать……..тот тригер был рервой идеей……..

        • Согласен, сделать можно. Но мне в голову приходит только выполнение запросов внутри рекурсивной функции (если не ограничивать уровень вложенности комментариев). А это может оказаться довольно ресурсоёмким решением.

  • michail1982

    Да, всё верно, после удаления бага его ИД не будет в подзапросе (select bug_id from bugs where replyto is NULL),
    соответственно все комментарии на него удаляться

    • Удаляться не только все комментарии к данному багу, но и ко всем остальным. Т.е. ВООБЩЕ ВСЕ комментарии. Останутся только записи о самих багах.
      У каждого комментария в поле replyto записано какое-то значение, либо id бага, либо id другого комментария (для комментариев с уровнем вложенности 2 и >).

      • michail1982

        это всего-лишь MyISAM………..
        при желании можно и чтото путёвое написать……..тот тригер был рервой идеей……..

        • Согласен, сделать можно. Но мне в голову приходит только выполнение запросов внутри рекурсивной функции (если не ограничивать уровень вложенности комментариев). А это может оказаться довольно ресурсоёмким решением.