Битва со списками или применение классов для вывода списков в PHP
Валентин Воронин
🕛 04.07.2006, 15:48
Список, список, список… Открываешь файл любого интернет-проекта, основанного на технологии PHP+MySQL, и первое, что видишь - это куски кода, примерно такого вида: $q = 'SELECT id, title FROM rubrics'; $result = mysql_query ($q) or die ('Select query failed'); print '<ol>'; while ($line = mysql_fetch_array ($result, MYSQL_ASSOC)) { print '<li><a href="?id=' . $line['id'] . '">'; print $line['title']; print '</a></li>'; } print '</ol>';
Подобным образом выводится список ссылок на отдельные страницы раздела сайта, хранящиеся в базе данных, фирмы в каталоге, подписчики рассылки в интерфейсе администрирования, статистика ресурса и многое другое.
Каждый раз, набирая подобный код в PHP-редакторе, видишь, что снова и снова проделываешь одну и ту же работу с минимальными вариациями. Различия только в коде SQL-запроса, типе и количестве выводимых полей и в HTML-тэгах, т.е. в дизайне. А если делаешь одну работу два раза, то это повод не только заскучать, но и задуматься о том, как ее себе облегчить. Да и попытки немного изменить дизайн сайта часто требуют модификации именно этого участка кода, а значит и его последующей отладки.
На протяжении нескольких лет работы с PHP у меня выработался определенный стандарт написания кода для вывода списков, и в результате захотелось обобщить этот опыт и сделать нечто стандартное, подходящее для любой ситуации, связанной с выводом результатов SQL-запроса в виде списка средствами PHP.
В результате все простые списки были препарированы на шесть частей (это самая простая их модель):
Обрамление списка: это то, внутри чего список находится, есть в нем хотя бы один элемент или нет.
Пустой список: если наш запрос не возвращает ни одной строки, то все-таки стоит вывести на сайт некоторое пояснение, например, „список пуст”, или „редактор в отпуске, новостей больше не будет”, или оставить здесь пустое значение.
Начало списка: если в списке есть хотя бы один элемент, то список можно начать какой-нибудь фразой, например, „начало списка”, или разместить здесь заголовок, который в случае пустого списка может и не выводиться.
Конец списка: нужен для тех же целей, что и предыдущий пункт - фраза „конец списка” или какие-либо итоговые данные по всем строкам, которые в случае пустого списка могут и отсутствовать.
Элемент списка: собственно это и есть вывод данных каждой строки запроса; в отличие от предыдущих пунктов этот элемент применяется к каждой строке запроса, выводя её в одном и том же дизайне внутри обрамления списка.
Разделитель элементов списка: иногда элементы списка отделяются чем-нибудь друг от друга - запятыми, вертикальными черточками, тире и прочим, причем в начале и конце списка эти разделители могут отсутствовать.
Вышеуказанную структуру я называю шаблоном списка. Остается написать код некоего класса, который, имея на входе SQL-запрос, порождает объект, содержащий результат выполнения этого запроса. В классе также требуется метод, который на основании разработанного выше шаблона выдаст нам требуемый HTML-код.
Можно, конечно, обойтись и функцией, которая выдаст требуемое, получив в качестве параметров SQL-запрос и код шаблона. Но на мой взгляд с классами и ООП в программировании работать куда вкуснее. Вдобавок я не собираюсь останавливаться на такой простой модели списка, а дальнейшие разработки кода с классами поддерживаются куда проще. А когда наши провайдеры сменят движки PHP с четвертой версии на пятую, классы войдут в PHP в том же объеме, что и в C++.
Недолго думая, класс был назван ListItems; незнакомых с английским отсылаю на multitran.ru. Пишем его код:
class ListItems { var $query; var $template; var $count = 0; var $items = array(); var $listitems = ''; function ListItems ($query='') { $this->query = $query; if (!empty ($this->query)) { $result = mysql_query ($this->query) or die('SQL query is failed'); while ($tmp = mysql_fetch_object ($result)) $this->items[] = $tmp; $this->count = count ($this->items); } } }
Конструктор класса принимает в качестве параметра код SQL-запроса. К моменту создания экземпляра класса у вас уже должна быть вызвана функция mysql_connect. После этого в свойстве items объекта мы получим результат выполнения запроса в виде нумерованного массива, каждый элемент которого является стандартным PHP-объектом, имена полей которого соответствуют именам полей запроса, а значения, естественно, соответствуют выбранным из базы данных значениям полей.
Теперь немного о шаблоне списка. Списки я препарировал, но ничего не сказал об их программной реализации. Для начала назовем каждый пункт каким-нибудь малопонятным именем на английском языке.
list - обрамление списка
empty - пустой список
begin - начало списка
end - конец списка
item - элемент списка
delimiter - разделитель элементов списка
Потом создадим массив, в котором каждый элемент шаблона индексируется принятым именем. Для написания кода элементов шаблона я пользуюсь одной достаточно распространенной идеей, вымученной PHP-программистами (да и не только ими) за многолетние попытки отделить дизайн от кода (эта статья также относится к ним). В настоящее время сложился определенный стандарт написания шаблонов для PHP-программ, использующий так называемые макрокоды, или макросы.
Чаще всего применяется конструкция, использующая угловые скобки с каким-либо текстовым или цифровым значением внутри для указания того места в шаблоне, куда надо вставлять нужную информацию. Для той же цели применяются теги комментариев HTML <!- -> с текстом или числом, которые идентифицируются PHP-движком и заменяются требуемой информацией.
На мой взгляд, главный недостаток такой технологии заключается в том, что если PHP по какой-либо причине не обработал этот элемент кода, то в браузере вы не увидите ничего, что указало бы на эту ошибку, поскольку комментарии HTML невидимы по определению, а конструкции с угловыми скобками выглядят для браузера как неизвестные ему HTML-тэги, которые также не отображаются. А если совершенная ошибка невидима, то она будет повторяться и никогда не отладится. К тому же шаблоны бывают достаточно сложны и объемны по содержанию и разрабатывать их приходится в каком-либо WISIWIG-редакторе. Попробуйте редактировать невидимые тэги и комментарии в нем, и сразу убедитесь, насколько это неудобно. Применение квадратных скобок в виде [name] не очень удачно, так как даже в обычных текстах они достаточно распространены.
В общем, для вставки в шаблоны варьируемой информации была придумана следующая конструкция: {%%NAME%%}, где NAME - это идентификатор того значения, которым мы хотим заменить этот макрос. Такой макрос виден в выводе браузера, в любом WISIWIG-редакторе, более того, многие популярные HTML- и PHP-редакторы исходного кода можно настроить на подсветку этой конструкции цветом, отличным от остального кода, поскольку в стандарте HTML и PHP такая последовательность символов не используется. К тому же этот макрос не требует для своего написания специальных символов HTML.
Идентификатор NAME должен подчиняться еще одному правилу. Как вы уже заметили, в коде класса ListItems, SQL-запрос преобразуется в массив объектов, у которых имена полей соответствуют именам полей запроса, а значения - значениям. Распространяя это дальше, примем, что идентификатор NAME должен соответствовать имени того поля, значение которого заменит макрос без учета регистра символов. То есть, если в вашей базе данных есть таблица, в которой в поле name содержатся названия стран мира, а в поле population - численность их населения, то для того чтобы вывести список стран в виде
НАЗВАНИЕ СТРАНЫ - НАСЕЛЕНИЕ
элемент item должен выглядеть так: {%%NAME%%} - {%%POPULATION%%} чел.
Чтобы указать, где в обрамлении списка выводится последовательность его элементов, а также их количество, в классе ListItems предусмотрены дополнительные поля и соответствующие им макросы.
listitems - в этом поле накапливается HTML-вывод по мере перебора элементов списка, а макрос {%%LISTITEMS%%} должен обязательно присутствовать в элементе list шаблона, поскольку он заменяется кодом всего списка.
count - это число элементов списка, а макрос {%%COUNT%%} заменяется значением этого поля.
С учетом вышесказанного, код, задающий шаблон, будет выглядеть так:
$template = array ( 'list' => '{%%LISTITEMS%%}', 'empty' => '<p>"Это мир, где нет стран и людей.', 'begin' =>' <p>Население {%%COUNT%%} стран мира.<ul>', 'end' => '</ul>', 'item' => '<li>{%%NAME%%} - {%%POPULATION%%} чел.</li>', 'delimiter' => ' ' );
Теперь приведу код метода объекта ListItems, который вставляет любой объект $o в каждый элемент шаблона $t, закодированного по определённым выше правилам.
function InsertObjectToTemplate ($o, $t) { foreach ($o as $n=>$v) if (gettype ($v)!= 'object' && gettype ($v)!='array') { $p = '\{%%' . strtoupper ($n) . '%%\}'; if (eregi ($p, $t)) $t = eregi_replace ($p, strval($v), $t); } return $t; }
Изменив регулярное выражение в третьей строке кода, вы можете изменить вид макроса в соответствии с принятым на вашем проекте. Например,
$p = '\[' . strtoupper ($n) . '\]';
позволит использовать макрос вида [name], а
$p = '\{[[:space:]]{0,}' . strtoupper ($n) . '[[:space:]]{0,}\}';
- такой же макрос с фигурными скобками и любым числом пробельных символов между name и скобками. Я использую в своих проектах макросы с возможностью добавления пробельных символов перед и просле name, и эта строка выглядит так:
$p = '\{%%[[:space:]]{0,}' . strtoupper ($n) . '[[:space:]]{0,}%%\}';
И последняя функция класса ListItems выводит массив объектов items в шаблон из шести элементов в соответствии со всем вышесказанным.
function getOutput ($arg) { $list = empty ($arg['list']) ? '' : $arg['list']; $begin = empty ($arg['begin']) ? '' : $arg['begin']; $end = empty ($arg['end']) ? '' : $arg['end']; $item = empty ($arg['item']) ? '' : $arg['item']; $delimiter = empty ($arg['delimiter']) ? '' : $arg['delimiter']; $empty = empty ($arg['empty']) ? '' : $arg['empty']; foreach ($this->items as $id=>$one) { $this->listitems .= $this->insertObjectToTemplate ($one, $item) . $delimiter; } $this->listitems = $this->count == 0 ? $empty : $begin . substr ($this->listitems, 0, -strlen ($delimiter)) . $end; $res = $this->insertObjectToTemplate ($this, $list); unset ($this->listitems); return $this->insertObjectToTemplate ($this, $res); }
Первые шесть строк кода нужны, чтобы предотвратить вывод сообщений об ошибках в случае несоответствия шаблона $arg введенным нами соглашениям. Далее по циклу перебираются все элементы массива items и вставляются в элемент item шаблона с добавлением разделителя delimiter. По окончании цикла, в случае, если количество элементов в items больше нуля, то listitems заменяется empty, если нет, то к нему добавляются begin и end с отбрасыванием последнего delimiter. Далее экземпляр нашего класса вставляется в list и еще раз в полученный результат но уже без поля listitems. Это необходимо для того, чтобы дать возможность разместить поле count в begin, end, item или delimiter, что иногда бывает необходимо.
В общем-то, уже почти все сказано. Но как полагается в любой статье по программированию, приведу один готовый к тестированию пример, чтобы мой читатель, который, как и всякий программист, достаточно ленив, мог оценить сей труд на своем сервере.
Скачайте файл listitems.rar и распакуйте его в один из каталогов вашего сервера. Код класса ListItems находится в файле cls_lstitems.inc, тестовый пример - в файле test.php, SQL-код для создания тестовой таблицы в вашей базе данных - в файле test.sql. Во второй-пятой строках файла test.php укажитете ваши данные для подключения к MySQL, создайте тестовую таблицу из файла test.sql и запускайте скрипт test.php. Результатом его работы будет список стран мира с их населением по последним данным из CIA World FactBook.
Раскомментируйте 20-ю строку скрипта. Теперь элемент item отличается от первоначального тэгами b и, соответственно, население страны выделяется жирным шрифтом. Раскомментируйте 22-ю и 23-ю строки, и список становится нумерованным. Если в 53-ей строке заменить параметр функции $template на $template1, то вы получите список в табличном виде. Шаблон $template2 позволяет вывести только названия всех стран мира через запятую с пробелом и завершающей точкой. Если 51-ую строку скрипта заменить на
$list = new ListItems ('');
то можно посмотреть на то, как класс ListItems обрабатывает пустые SQL-запросы.
Конечно, код класса ListItems намного сложнее приведенного в начале статьи цикла. Но, вводя такой класс, мы сделали одну очень большую вещь. В этом примере программный код (класс) полностью отделен от дизайна (шаблон) и содержащейся в выводимом списке информации (SQL-запрос). Добавьте к этому возможность хранить любое количество шаблонов в базе данных за счет их однотипной структуры, выбирая их по имени или номеру (идентификатору) и редактируя через какой-нибудь интерфейс администрирования без необходимости правки программного кода. Теперь не надо запускать дизайнеров в программный код, достаточно лишь поправить шаблон. Нет необходимости править код и в случае добавления новых данных: если вы хотите добавить в ваш список площадь территории каждой страны, то нужно лишь добавить в таблицу MySQL новое поле square, а в поле item шаблона - макрос {%%SQUARE%%}. Все это очень сильно облегчает жизнь разработчику движка сайта и CMS (Content Management System).
В этой статье мною приведена самая простая модель шаблона списка. На данный момент код класса ListItems, применяемый мной, куда сложнее. На сайте http://asiadata.ru/, который я поддерживаю как вебмастер, этот класс выводит все списки, позволяя группировать их по любому количеству элементов, выделять один или несколько из них, сортировать по одному или нескольким полям, как в запросе, так и в самом массиве items, а также расставлять ссылки и многое другое. Если читателей статьи заинтересует написанное, пишите на mailto:direqtor@mail.ru, и я продолжу и разовью затронутую здесь тему.