РНР: преимущества и особенности
Денис Нивников
Всемирная сеть меняется очень быстро. Кажется, еще на прошлой неделе мы восхищались бездной информации, представленной в виде гипертекстовых документов. Несколько дней назад оценивающе рассматривали яркие презентационные сайты, изобилующие оригинальными дизайнерскими решениями, и смаковали новый термин — "динамический HTML". Сегодня под словами "динамический Web-сайт" мы стали подразумевать динамическое содержание, а не динамическое оформление.
В современном Интернете не много найдется сайтов, информация на которых не обновлялась бы хотя раз в месяц — это полезно как для пользователей, так и для поставщиков информации. Разработчикам доступно множество инструментов, с помощью которых можно создавать системы практически любой сложности — от сайтов, предоставляющих информацию и средства общения для небольших групп единомышленников, до крупнейших информационных центров, выдерживающих тысячи и десятки тысяч обращений в сутки. При выборе конкретного средства разработки следует учитывать не только потенциальную производительность системы, но и такие факторы, как стоимость разработки, переносимость созданной системы и совместимость выбранной платформы с новыми, еще только внедряемыми технологиями.
Что такое РНР?
В том виде, в которым PHP знаком программистам сейчас, этот язык появился в 1997 г., хотя аббревиатура PHP появилась намного раньше — в 1995 г. был подготовлен проект PHP/FI (Personal Home Page/Forms Interpreter), представляющий собой набор Perl-сценариев для подсчета обращений к размещенному в Интернете резюме автора — Расмуса Лердорфа (Rasmus Lerdorf). Позже автор значительно расширил функциональность PHP/FI, полностью переписав приложение на С. Новая версия значительно больше походила на современный РНР, в нее была включена поддержка соединений с базами данных и средства разработки простейших динамических страниц. В ноябре 1997 г. была практически готова вторая версия PHP/FI, однако в то же время два программиста — Энди Гутманс (Andi Gutmans) и Зив Зуразки (Zeev Suraski) — разработали третью версию PHP (теперь именно РНР, без FI — расшифровывается как PHP Hypertext Processor), полностью переписав его ядро. Хотя авторы у третьей версии и были другие, по договоренности с Расмусом ее решено было считать преемником второй версии PHP/FI, и работы над последней были прекращены.
Третья версия PHP завоевала популярность благодаря широкому набору стандартных функций, совместимости с большинством распространенных протоколов и баз данных. Гибкий, С-образный язык сценариев, встраиваемых непосредственно в тело HTML-документа, простота и удобство работы с массивами и возможность использования объектно-ориентированного программирования (ООП) сделали РНР одной из самых распространенных технологий Интернета. Безусловно, немаловажен и тот факт, что с самого начала разработки РНР был проектом с открытым исходным кодом. Благодаря этому РНР остается одной из самых доступных технологий построения динамических сайтов, будучи при этом и одной из самых мощных.
Однако не обошлось и без недостатков — в третьей версии РНР не очень удачно реализованы методы ООП, трудно назвать выдающейся и производительность ядра в сложных приложениях, поэтому всего через несколько месяцев после официального выхода третьей версии Гутманс и Зуразки начали работу над четвертой. Эта версия РНР была представлена в мае 2000 года. Хотя с точки зрения программиста язык сценариев изменился незначительно, ядро РНР было почти полностью переписано. В настоящее время четвертая версия — последняя официально представленная, однако уже начата разработка пятой версии РНР.
Почему РНР?..
…а не, например, Perl, ASP, ColdFusion или еще какая-нибудь серверная технология? В чем преимущества РНР?
Во-первых, РНР — технология мультиплатформная. В настоящее время с Web-сайта php.net можно загрузить скомпилированные версии для Win32, MacOS и RISC OS; кроме того, PHP входит в состав инсталляционных пакетов большинства Linux-систем. Все это обеспечивает высокую переносимость проектов.
Во-вторых, PHP — это проект с открытым исходным кодом. Это означает более динамичное совершенствование продукта и исправление ошибок.
В-третьих, общая стоимость проекта, созданного на базе Unix/Linux + Apache + PHP + MySQL/PostgreSQL, значительно ниже, чем у большинства других средств разработки при той же надежности и мощности. Упомянутые базы данных и Web-сервер, так же, как и РНР, входят в инсталляционные пакеты Linux, а значит, обеспечена высокая скорость развертывания Web-сервера. В то же время это вовсе не означает, что РНР ориентирован на небольшие бесплатные базы данных — в нее включены функции взаимодействия с десятком самых распространенных СУБД, среди которых Microsoft SQL и Oracle.
В-четвертых, РНР можно подключать как модуль Apache, что еще больше повышает быстродействие.
В-пятых… А впрочем, стоит ли продолжать? РНР изначально проектировался именно как продукт для построения динамических Web-сайтов и за 5 лет своего активного развития превратился в мощнейшее средство разработки.
Особенности проектирования
РНР — это язык сценариев, встраиваемый в HTML-код. При каждом обращении к РНР-сценарию Web-сервер считывает код, интерпретирует и выполняет его, после чего, сформировав результирующий HTML-код, возвращает его пользовательскому агенту — браузеру. Как уже упоминалось, РНР может быть сконфигурирован как модуль Apache — это дает некоторый прирост производительности, но лишает программиста возможности выполнять сценарий с правами доступа того или иного пользователя.
Одна из отличительных особенностей РНР — автоматическое преобразование типов переменных. Благодаря этому одна и та же переменная может использоваться в арифметическом выражении, обрабатываться как строка и быть передана условному оператору. Тут необходимо отметить, что при приведении типа переменной к логическому типу любое число, кроме нуля, любая строка, кроме пустой, и любой массив, содержащий хотя бы один элемент, воспринимаются сценарием как true, в противном случае — как false. Это придает сценариям РНР гибкость и простоту, но в то же время требует от программиста большей внимательности.
Возможность использования объектно-ориентированного подхода позволяет создавать достаточно сложные структурированные приложения, однако не стоит забывать, что сценарий интерпретируется заново при каждом обращении. Это значит, что строки дополнительного кода, описывающие классы, помимо упрощения структуры и удобства для программиста создадут еще и ощутимую нагрузку на сервер. Зачастую использование классов в РНР оказывается неоправданным и имеет смысл только для крупных проектов, функционирующих на высокопроизводительном аппаратном обеспечении. Для большинства решаемых с помощью РНР задач вполне достаточно процедурного подхода. Сложные же типы данных легко реализуются с помощью массивов.
Пример приложения: служба почтовых рассылок
Чтобы проиллюстрировать некоторые из описанных преимуществ языка РНР, решим с его помощью какую-нибудь распространенную задачу. Например, создадим систему рассылки почтовых сообщений — системные администраторы нередко устанавливают на сервере специальные программы, что позволяет предоставить сотрудникам, отвечающим за корпоративные рассылки, дополнительную функциональность, а кроме того, перенести нагрузку с клиентской машины на сервер и разгрузить ЛВС.
Описывая процесс создания службы рассылки, будем предполагать, что на нашем сервере уже установлена БД MySQL, Apache и PHP как модуль Web-сервера.
Для начала определим минимальный набор функций, выполняемый нашей программой.
Она должна:
- создавать список подписчиков с возможностью добавления, удаления и редактирования
каждого; - создавать список групп подписчиков с аналогичными возможностями управления
им; - рассылать сообщение всем подписчикам одной или нескольких выбранных групп;
- вести журнал рассылок.
Подготовим базу данных maillist — создадим таблицы для хранения информации о подписчиках, группах подписчиков и журнала рассылок, а также заведем нового пользователя для доступа к этой базе:
CREATE DATABASE maillist; USE maillist; GRANT ALL PRIVILEGES ON maillist.* TO maillist@localhost IDENTIFIED BY 'bytemag'; CREATE TABLE `subscriber` ( `id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, `groupid` SMALLINT UNSIGNED NOT NULL, `name` VARCHAR(255) NOT NULL, `email` VARCHAR(255) NOT NULL, `descr` VARCHAR(255) ); CREATE TABLE `list` ( `id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, `name` VARCHAR(255) NOT NULL, `descr` VARCHAR(255) ); CREATE TABLE `log` ( `id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, `filldate` TIMESTAMP NOT NULL, `subj` VARCHAR(255) NOT NULL, `size` SMALLINT UNSIGNED NOT NULL, `groupid` SMALLINT UNSIGNED NOT NULL ); |
Мы не станем подробно описывать значение каждого поля таблицы, отметим только, что для упрощения структуры БД и файлов сценариев введено ограничение — каждый подписчик может входить только в одну группу. Безусловно, нам следовало бы исключить поле groupid из таблицы subscriber, а для установки связей между группами и подписчиками создать таблицу отношений — в этом случае каждый подписчик мог бы входить в состав нескольких групп. Однако для нашего приложения, носящего сугубо иллюстративный характер, такое ограничение вполне допустимо — оно не скажется заметным образом на быстродействии, но значительно упростит сценарии.
Приложение будет состоять из нескольких (если быть точным — из шести) сценариев, отвечающих за связь с базой данных, редактирование записей, отправку сообщения и просмотр журнала рассылок.
db.php <? // Устанавливаем соединение с базой // данных $host = "localhost"; $login = "maillist"; $passwd = "bytemag"; $conn_error = "<b>Невозможно подключиться к базе данных!</b>"; $mysql_link = @mysql_connect ($host, $login, $passwd) or exit ($conn_error); @mysql_select_db('maillist') or exit ($conn_error); function insert_into_db($tb_name, $vals) { if ($vals) { $fields = ''; $values = ''; while (list($col, $val) = each($vals)) { if ($fields) { $fields .= ", "; $values .= ", "; }; $fields .= $col; $values .= "'".$val."'"; }; $query = "REPLACE INTO $tb_name ($fields) VALUES ($values)"; return mysql_query($query); }; }; ?> |
Процедуру подключения к БД имеет смысл вынести в отдельный сценарий и подключать в других сценариях с помощью функции require() — это дает возможность легко перенастраивать переменные подключения в нескольких независимых сценариях. Кроме того, мы подготовили процедуру insert_into_db() для сохранения данных в БД. В качестве параметров этой процедуре передается имя таблицы и ассоциативный массив значений, ключами которого служат названия полей в таблице. Оператор replace в запросе SQL, обнаружив в таблице уже существующую строку с аналогичными передаваемым значениями уникальных полей, заменит ее новой — в отличие от оператора insert, сообщающего в этом случае об ошибке. Таким образом, применение replace позволит нам одинаково легко вставлять новые строки и заменять отредактированные.
На первый взгляд может показаться странным, что мы не написали соответствующую процедуру для извлечения строк из таблицы, однако на практике полезность такой функции оказывается сомнительной из-за разнообразия возможных запросов. С точки зрения улучшения быстродействия в небольших сценариях удобнее пользоваться стандартными функциями PHP, а при написании сложных приложений лучше подготовить специальный класс — в дальнейшем это, помимо других преимуществ ООП, облегчит переносимость приложения.
Вернемся к нашей задаче. В первую очередь нам необходимы средства добавления и редактирования списков рассылки и подписчиков. Мы не сможем пользоваться приложением до тех пор, пока в базе данных не зарегистрирован ни один список рассылки, поэтому в первую очередь подготовим сценарий, добавляющий и редактирующий информацию о списках рассылки. Вызванный без параметров, он должен подготовить форму для новых данных, а при передаче номера списка рассылки — такую же форму, но заполненную соответствующими данными:
group.php <? require('db.php'); if ($HTTP_POST_VARS) { if (insert_into_db('list', $HTTP_POST_VARS)) { $saved_ok = "<i>сохранено</i>"; } else { $saved_ok = "<b>Ошибка! Данные не сохранены!</b>"; }; }; if ($HTTP_GET_VARS['id']) { $query = "SELECT * FROM list WHERE id='".$HTTP_GET_VARS['id']."'"; $result = mysql_query($query); $list = mysql_fetch_array($result); }; ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Список рассылки</title></head> <body> [ <a href="index.php">в начало</a> ] <?=$saved_ok?> <hr> <form action="group.php" method="post"> <input type="hidden" name="id" value=' <?=$list['id']?>'> <table border="0" cellpadding="3" cellspacing="0"> <tr> <td>Название:</td> <td><input type="text" size="25" name="name" value='<?= $list['name']?>'></td> </tr> <tr> <td>Примечание:</td> <td><input type="text" size="25" name="descr" value='<?= $list['descr']?>'></td> </tr> <tr> <td colspan=2><input type=submit value="подтвердить"></td> </tr> </table> </form> </body> |
Начинаем сценарий с подключения файла db.php и устанавливаем соединение с базой данных. Оператор require(), в отличие от оператора include(), прервет работу сценария, если произойдет ошибка чтения файла, — это гарантирует, что дальнейшие инструкции будут выполнены только в том случае, когда соединение с базой прошло успешно.
Затем мы проверяем, не переданы ли сценарию какие-либо данные. Параметры, передаваемые методом POST, содержатся в ассоциативном массиве HTTP_POST_VARS, а методом GET — в HTTP_GET_VARS. Здесь необходимо отметить, что при определенных настройках (в том числе и принимаемых по умолчанию) PHP обладает замечательным свойством, значительно облегчающим программирование: при инициализации сценария эти массивы "разбираются" на переменные. Иначе говоря, программисту доступны идентичные переменные, например, $HTTP_POST_VARS['var'] и $var. Несмотря на подкупающее удобство этого подхода, мы не рекомендовали бы использовать данную возможность при разработке серьезных проектов, так как это, во-первых, отрицательно сказывается на переносимости приложения, а во-вторых, при недостаточно аккуратном программировании делает сценарий более уязвимым для злоумышленников. Не безупречен и предложенный нами вариант — для максимальной защищенности сценария следовало бы добавить проверку источника полученных данных (getenv('HTTP_REFERER')). Кроме того, при работе с базой данных следует использовать функции addslashes() и stripslashes(), а при выводе данных в HTML — функции htmlspecialchars() и htmlentities().
После заполнения формы и нажатия кнопки "подтвердить" данные будут переданы сценарию и сохранены им с помощью уже описанной функции insert_into_db(). Сообщение о результатах записи заносится в переменную $saved_ok, а форма отображается незаполненной. Если сценарий был вызван с параметром id в строке URL, то в базе ищется строка с соответствующим номером и создается ассоциативный массив $list, значениями которого потом заполняется форма.
Сценарий, используемый для редактирования данных о подписчике, почти не отличается от сценария редактирования списков рассылки. Единственное отличие — функция groups_list(), которая создает выпадающий список и заносит в него названия списков рассылки.
subscriber.php <? require('db.php'); function groups_list() { $result = mysql_query("SELECT id, name FROM `list` ORDER BY id"); if (mysql_num_rows($result)) { while ($next = mysql_fetch_array ($result)) { if ($next['id'] == $subscriber['groupid']) { echo "<option value='".$next['id'] ."' selected>".$next['name']; } else { echo "<option value='".$next['id']. "'>".$next['name']; }; }; }; }; if ($HTTP_POST_VARS) { if (insert_into_db('`subscriber`', $HTTP_POST_VARS)) { $saved_ok = "<i>сохранено</i>"; }; }; if ($HTTP_GET_VARS['id']) { $result = mysql_query("SELECT * FROM `subscriber` WHERE id='" .$HTTP_GET_VARS['id']."'"); $subscriber = mysql_fetch_array($result); }; ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head><title>Подписчик</title></head> <body> [ <a href="index.php">в начало</a> ] <?=$saved_ok?> <hr> <form action="subscriber.php" method="post"> <input type=hidden name=id value=<?= $subscriber['id']?>> <table border="0" cellpadding="3" cellspacing="0"> <tr> <td>Имя:</td> <td><input type="text" size="25" name="name" value='<?= $subscriber['name']?>'></td> </tr> <tr> <td>e-mail:</td> <td><input type="text" size="25" name="email" value='<?= $subscriber['email']?>'></td> </tr> <tr> <td>Примечание:</td> <td><input type="text" size="25" name="descr" value='<?= $subscriber['descr']?>'></td> </tr> <tr> <td>Группа:</td> <td> <select name="groupid"> <? groups_list(); ?> </select> </td> </tr> <tr> <td colspan=2><input type=submit value="подтвердить"></td> </tr> </table> </form> </body> |
Основную функцию нашего приложения — рассылку сообщений по списку адресов выполняет сценарий message.php. Для отправки сообщений в нем используется стандартная функция PHP mail(). Перед вызовом этой функции создается список адресов подписчиков (переменная $to), входящих в выбранные группы. Нередко авторы почтовых рассылок допускают ошибку, помещая список адресов в поле "To:" или "Cc:", — в результате этого каждый адресат получает полный список подписчиков и адресов их электронной почты. Избежать этого достаточно легко — список адресов необходимо поместить в поле "Bcc:" (скрытая копия).
message.php <? include('db.php'); function groups() { $query = "select id, name from list"; $result = mysql_query($query); if (mysql_num_rows($result)) { while ($next = mysql_fetch_array ($result)) { echo "<input type="checkbox" name="group[]" value="".$next ['id']."">".$next['name']."<br> "; }; }; }; function sendmail($subj, $text, $lists) { while (list($idx, $list) = each($lists)) { $query = "select name, email from subscriber where groupid='".$list."'"; $result = mysql_query($query); if (mysql_num_rows($result)) { $to = ""; while ($subscriber = mysql_fetch_array($result)) { $to .= (($to) ? ", " : ""); $to .= """.$subscriber['name']."" <".$subscriber['email'].">"; }; }; if ($to && $text) { global $send_ok; if (@mail(""MailList"", $subj, $text, "Bcc: ".$to." Content-Type: text/plain; charset=windows-1251")) { $send_ok = "<i>Сообщение отправлено!</i>"; insert_into_db("log", array('subj' => $subj, 'size' => strlen($text), 'groupid' => $list)); } else { $send_ok = "<b>Ошибка при отправке сообщения!</b>"; }; }; }; }; if ($HTTP_POST_VARS) { if ($HTTP_POST_VARS['group']) { sendmail($HTTP_POST_VARS['subj'], $HTTP_POST_VARS['text'], $HTTP_POST_VARS['group']); }; }; ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head><title>New Message</title></head> <body> [ <a href="index.php">в начало</a> ] <?=$send_ok?> <hr> <form action="message.php" method="post"> <table border=0 cellpadding=0 cellspacing=5> <tr> <td>Тема:</td> <td></td> </tr> <tr> <td><input type=text size="40" name="subj"></td> <td></td> </tr> <tr> <td>Текст:</td> <td></td> </tr> <tr> <td valign="top"><textarea cols="50" rows="20" name="text"></textarea></td> <td valign="top" rowspan="2"><? groups(); ?></td> </tr> <tr> <td align="right"><input type="submit" value="отправить"></td> </tr> </table> </form> </body> </html> |
Этот сценарий создает форму, размещая в ней с помощью процедуры groups(), помимо полей "Тема" и "Текст", переключатели для выбора списков рассылки, по которым будет отправлено сообщение. В случае успешной отправки сообщения в журнал записываются дата и время отправки, тема сообщения и его размер.
Приведенный сценарий хорошо иллюстрирует легкость работы с массивами, присущую языку PHP. Внимательные читатели могли заметить, что имена этих переключателей совпадают — для многих языков сценариев, применяемых для обработки HTML-форм, это было бы недопустимо, но для PHP это стандартный способ передать сценарию индексированный массив значений. Еще один удобный прием — создание массива "на лету": вызывая функцию insert_into_db(), мы с помощью стандартной функции array() строим ассоциативный массив, используя в нем как переменные, так и результаты других функций.
Итак, для организации простейшей системы рассылок нам понадобилось меньше 140 строк кода. Конечно, ее функциональность минимальна — мы пренебрегли гибкостью приложения в пользу простоты и не реализовали возможности присоединения файлов и создания сообщений в разных кодировках и форматах. Впрочем, это приложение преследовало совсем другую цель — мы продемонстрировали сочетание простоты, гибкости и мощности, свойственное языку PHP.
В заключение приведем еще два сценария, призванных улучшить интерфейс нашего приложения, — это файлы log.php и index.php. Первый отображает записи журнала рассылок, а второй выводит списки подписчиков и зарегистрированных групп и организует навигацию между сценариями приложения.
log.php <? include('db.php'); function log_records() { $query = "select *, DATE_FORMAT (filldate, "%H:%i %d/%m/%Y") as date from log order by filldate desc"; $result = mysql_query($query); if (mysql_num_rows($result)) { echo "<tr><th>Дата и время</th> <th>Тема рассылки</th> <th>Размер сообщения</th></tr>"; while ($next = mysql_fetch_array($result)) { echo "<tr><td>".$next['date']."</td> <td align=right>".$next['subj']." </td><td align=right> ".$next['size']."</td></tr>"; }; } else { echo "<tr><td><b>Журнал рассылок пуст!</b></td></tr>"; }; }; ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head><title>Журнал рассылок</title></head> <body> [ <a href="index.php">в начало</a> ] <hr> <table border=0 cellpadding=3 cellspacing=0> <? log_records(); ?> </table> </body> </html> index.php <? require('db.php'); function user_list() { $query = "SELECT s.id, s.name, s.email, s.groupid, g.name AS groupname FROM `subscriber` AS s, `list` AS g " ."WHERE s.groupid=g.id ORDER BY groupname, name"; $result = mysql_query($query); if (mysql_num_rows($result)) { echo "<table border=0 cellspacing=0 cellpadding=3> " ."<tr><th>Имя:</th><th>Адрес:</th> <th>Группа:</th><th></th></tr> "; while ($next = mysql_fetch_array($result)) { echo "<tr><td>".$next['name']."</td> <td><a href=mailto: ".$next['email'].">" .$next['email']."</a></td><td>" .$next['groupname']."</td> <td>[<a href=subscriber.php?id= ".$next['id'].">редактировать</a>] </td></tr>"; }; echo "</table>"; } else { echo "Не зарегистрировано ни одного подписчика!"; }; }; $result = mysql_query("SELECT * FROM `list`"); if (mysql_num_rows($result)) { $menu = "[ <a href=message.php>Новое сообщение</a> ] [ <a href=subscriber.php>Добавить подписчика</a> ] [ <a href=group.php> Добавить список рассылки</a> ] [ <a href=log.php>Журнал рассылок</a> ]<hr>"; $groups = "<table border=0 cellpading=2 cellspacing=0>"; while ($next = mysql_fetch_array($result)) { $groups .= "<tr><td>".$next['name']." </td><td>[ <a href=group.php?id= ".$next['id'].">редактировать</a> ] </td></tr>"; }; $groups .= "</table>"; } else { $menu = "<b>Не найдено ни одной группы подписчиков! <br>" ."Пожалуйста, [ <a href=group.php> зарегистрируйте</a> ] хотя бы одну, прежде чем начать работать с системой!<br></b>"; }; ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head><title>Система управления списками рассылки</title></head> <body> <center> <?=$menu?> <? if ($groups) :?> <table border=1 cellpadding=3 cellspacing=0> <tr> <th>Подписчики:</th> <th>Группы:</th> </tr> <tr> <td valign="top"><? user_list(); ?> </td> <td valign="top"><?=$groups?></td> </tr> </table> <? endif; ?> </center> </body> </html> |