САМОУЧИТЕЛЬ PHP 4

         

Переменные и массивы


array compact(mixed $vn1 [, mixed $vn2, …])

Функция compact(), впервые появившаяся в PHP версии 4, упаковывает в массив переменные из текущего контекста (глобального или контекста функции), заданные своими именами в $vn1, $vn2

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

$a="Test string";

$b="Some text";

$A=compact("a","b");

// òåïåðü $A===array("a"=>"Test string", "b"=>"Some text")

Почему же тогда параметры функции обозначены как mixed? Дело в том, что они могут быть не только строками, но и списками строк. В этом случае функция последовательно перебирает все элементы этого списка, и упаковывает те переменные из текущего контекста, имена которых она встретила. Более того — эти списки могут, в свою очередь, также содержать списки строк, и т. д. Правда, последнее используется сравнительно редко, но все же вот пример:

$a="Test";

$b="Text";

$c="CCC";

$d="DDD";

$Lst=array("b",array("c","d"));

$A=compact("a",$Lst);

// òåïåðü $A===array("a"=>"Test", "b"=>"Text", "c"=>"CCC", "d"=>"DDD")

void extract(array $Arr [, int $type] [, string $prefix])

Эта функция производит действия, прямо противоположные compact().

А именно, она получает в параметрах массив $Arr

и превращает каждую его пару ключ=>значение в переменную текущего контекста.

Параметр $type предписывает, что делать, если в текущем контексте уже существует переменная с таким же именем, как очередной ключ в $Arr. Он может быть равен одной из констант, перечисленных в табл. 13.1



Таблица 13.1. Поведение функции extract в случае совпадения переменных

Константа

Действие

EXTR_OVERWRITE

Переписывать существующую переменную (по умолчанию)

EXTR_SKIP

Не перезаписывать переменную, если она уже существует

EXTR_PREFIX_SAME

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

EXTR_PREFIX_ALL

Всегда предварять имена создаваемых переменных префиксом $prefix

<
По умолчанию подразумевается EXTR_OVERWRITE, ò. å. переменные перезаписываются. Вот пара примеров применения этой функции:

// Сделать все переменные окружения глобальными

extract($HTTP_ENV_VARS);

// То же самое, но с префиксом E_

extract($HTTP_ENV_VARS,

EXTR_PREFIX_ALL, "E_");

echo $E_COMSPEC; // выводит переменную окружения COMSPEC



Параметр $prefix

имеет смысл указывать только тогда, когда вы применяете режимы EXTR_PREFIX_SAME или EXTR_PREFIX_ALL.

Вообще

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

<table width=100%>

<?foreach($Book as $Entry) { extract($Entry)?>

  <tr>

    <td>Èìÿ: <?=$name?></td>  <!-- вместо $Entry['name'] -->

    <td>Àäðåñ: <?=$url?></td> <!-- вместо $Entry['url']  -->

  </tr>

  <tr><td colspan=3><?=$text?></td></tr>

  <tr><td colspan=3><hr></td></tr>

<?}?>

</table>

Здесь вы должны загодя позаботиться, чтобы ключи $Entry ненароком не затерли нужные переменные. Этого можно добиться, например, назвав все важные переменные с прописной буквы (например, $Book и $Entry), а все ключи — с маленькой, как и было сделано немного выше.


Переменные, константы, выражения


Как вы, наверное, уже заметили, структура PHP-программы довольно сильно напоминает смесь Бейсика и Си, да еще со включениями на HTML. Что ж, так оно, в общем, и есть. Однако мы рассмотрели лишь очень простой пример программы на PHP, поэтому вряд ли сможем сейчас увидеть общую картину языка. А теперь настало время заняться конструкциями PHP вплотную.

Начнем мы с основ языка. Итак...



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


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



Перемешивание списка shuffle()


Функция shuffle() "перемешивает"

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

$A=array(10,20,30,40,50);

shuffle($A);

foreach($A as $v) echo "$v  ";

Приведенный фрагмент выводит числа 10, 20, 30, 40 и 50 в случайном порядке.

Выполнив этот фрагмент несколько раз, вы можете обнаружить, что от запуска к запуску очередность следования чисел не изменяется. Это свойство обусловлено тем, что функция shuffle()

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



Переворачивание массива array_reverce()


Функция array_reverse()

возвращает массив, элементы которого следуют в обратном порядке относительно массива, переданного в параметре. При этом связи между ключами и значениями, конечно, не теряются. Например, вместо того, чтобы ранжировать массив в обратном порядке при помощи arsort(), мы можем отсортировать его в прямом порядке, а затем перевернуть:

$A=array("a"=>"Zero","b"=>"Weapon","c"=>"Alpha","d"=>"Processor");

asort($A);

$A=array_reverse($A);

Конечно, указанная последовательность работает дольше, чем один-единст­венный вызов arsort().



Перспективы: создание "умной" функции для отправки писем


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

bool PostMail(string $ToAddress, string $Encode, string $Message)

Посылает письмо $Message по адресу $ToAddress, перекодировав его предварительно в кодировку, заданную в $Encode. Параметр $Encode может принимать следующие значения:

r    w — Windows

r    k — KOI8-R

r    m — Mac

r    i — Iso Latin

r    t — Translit

В письме автоматически проставляется Content-type...charset (если заголовок Content-type уже присутствует в письме, то он не портится, а просто у него меняется поле charset на нужное значение[E82] ). Также корректируется поле To в письме. Одновременно правильно обрабатываются вставки PHP-кода в тело письма (можно использовать глобальные переменные и оператор echo). Для этого, как обычно, применяются "скобки" <? и ?>.

Реализацию поставленной задачи мы отложим до части V, где описаны и другие приемы, облегчающие работу на PHP. Для этого нам понадобится техника регулярных выражений, которыми мы вскоре займемся, а также еще некоторые навыки. Если вы уже сейчас хотите использовать функцию PostMail(), можете сразу открыть часть V книги и скопировать оттуда ее исходный код на PHP.



Почтовые шаблоны


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

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



Поддержка механизма поиска включаемых файлов


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

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

На примере использования библиотекаря мы уже убедились, насколько утомительным может быть указание абсолютных путей к файлам. Поэтому функция Load() умеет сама искать включаемые файлы по серверу. Она делает это всякий раз, когда ей задан относительный путь к файлу. Поиск ведется на основе списка так называемых каталогов для поиска шаблонизатора. Этот список можно пополнять с помощью вызова Inc(), как это сделано, например, в листинге30.12. Функция Inc() довольно интеллектуальна: даже если ей передан относительный путь к каталогу, она переводит его в абсолютный. Так что при использовании Load() из файла, расположенного в другом каталоге, не происходит никаких недоразумений.



Поддержка трехуровневой схемы разработки сценариев


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

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

С загрузкой интерфейсов посредством Uses() все обстоит несколько сложнее. Вполне может возникнуть ситуация, когда один и тот же интерфейс требуется в разных местах шаблона страницы для выполнения различных действий. Функция же Uses() всегда загружает файл лишь однажды, следя за тем, чтобы в следующий раз ее вызов был просто проигнорирован. Так что она нам не совсем подходит. В качестве альтернативы предлагается добавить в код библиотекаря еще одну функцию (назвав ее, например, UsesMulti()), которая могла бы загружать указанный файл несколько раз. Единственное отличие ее кода от кода Uses()

состоит в том, что она использует инструкцию include, а не include_once. Написание этой функции предоставляю читателю.

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



Поддержка закачки в PHP


Так как PHP специально разрабатывался как язык для Web-приложений, то, естественно, он "умеет"

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



Поиск записей


select * from Òàáëèöà where Âûðàæåíèå [order by ÈìÿÏîëÿ [desc]]

Эта команда— основная и очень мощная. Предназначена она для того, чтобы искать все записи, удовлетворяющие выражению Выражение. Ее возможности гораздо более богаты, чем то сжатое изложение, которое я вам предлагаю, и о них можно прочитать в книгах, посвященных SQL. Если записей несколько, то при указанном предложении order by они будут отсортированы по тому полю, имя которого записывается правее этого ключевого слова (если задан описатель desc, то упорядочивание происходит в обратном порядке). В предложении order by могут также задаваться несколько полей.

Особое значение имеет символ *. Он предписывает, что из отобранных записей следует извлечь все

поля, когда будет выполнена команда получения выборки. С другой стороны,

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



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


<input type=password

  name=èìÿ

  [value=çíà÷åíèå]

  [size=ðàçìåð]

  [maxlen=÷èñëî]

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



Полиморфизм


Полиморфизм

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

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

Вернемся к нашему предыдущему примеру с классами A и B.

class A {

 // Выводит, функция какого класса была вызвана

 function Test() { echo "Test from A\n"; }

 // Тестовая функция — просто переадресует на Test()

 function Call() { Test(); }

}

class B extends A {

 // Функция Test() для класса B

 function Test() { echo "Test from B\n"; }

}

$a=new A();

$b=new B();

Давайте рассмотрим следующие команды:

$a->Call(); // напечатается "Test from A"

$b->Test(); // напечатается "Test from B"

$b->Call(); // Внимание! Напечатается "Test from B"!

Обратите внимание на последнюю строчку: вопреки ожиданиям, вызывается не функция Test() из класса A, а функция из класса B! Складывается впечатление, что Test() из B просто переопределила

функцию Test() из A. Так оно на самом деле и есть. Функция, переопределяемая в производном классе, называется виртуальной.

Механизм виртуальных функций позволяет нам, например, "подсовывать" функциям, ожидающим объект одного класса, объект другого, производного, класса. Еще один классический пример — класс, воплощающий собой свойства геометрической фигуры, и несколько производных от него классов — квадрат, круг, треугольник и т. д. Базовый класс имеет виртуальную функцию Draw(), которая заставляет объект нарисовать самого себя. Все производные классы-фигуры, разумеется, переопределяют эту функцию (ведь каждую фигуру нужно рисовать по-особому). Также у нас есть массив фигур, причем мы не знаем, каких именно. Зато, используя полиморфизм, мы можем, не задумываясь, перебрать все элементы массива и вызвать для каждого из них метод Draw() — фигура сама


"решит", какого она типа и как ее рисовать.

В нашем классе MysqlTable, который мы еще только-только наметили, идея полиморфизма найдет свое применение. И вот зачем. Мы проектируем класс так, чтобы другие классы, которые он будет использовать, подключали его к себе как производный. Тем самым они наследуют все свойства MysqlTable и добавляют некоторые свои. Например, класс Guestbook, реализующий гостевую книгу, может быть производным от MysqlTable и "расширять"

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

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



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


Полноценный класс таблицы MySQL


Я ранее обещал, что в каждой главе части V книги обязательно будет присутствовать пример нетривиального кода на PHP, который (или идеи из которого) вы сможете использовать в своих программах. На этот раз "исходник"

оказался особенно большим, но это с лихвой оправдывается его функциональностью. Сейчас мы с вами разработаем полноценный класс, который существенно облегчает работу с таблицей MySQL, в значительной степени абстрагируя программиста не только от специфики этой СУБД, но и вообще от сложностей SQL-запросов. С помощью этого класса даже начинающий программист сможет построить форум, гостевую книгу, да и вообще любую программу, которая требует структурированного хранилища данных большого объема. Правда, для того, чтобы извлекать максимальную выгоду из использования класса, придется разобраться в механизме наследования, вкратце описанном чуть выше. Впрочем, класс прекрасно работает и сам по себе. Вот его некоторые отличительные черты.

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

r    Таблица является с точки зрения программиста набором записей совершенно произвольной структуры (с произвольным числом полей). При создании таблицы указываются лишь ее несущие

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

для вызывающей программы.

r    В то же время имеется возможность добавления/удаления несущих столбцов "на лету", т. е. без какого бы то ни было специального запроса пользователя. Достаточно изменить список несущих полей при создании/открытии таблицы. Класс сам определяет, что именно изменилось, и применяет соответствующие действия по корректировке (вызывает нужные команды SQL).


r    Поддерживается одно автоинкрементное поле с именем id, которое автоматически проставляется у записи при ее добавлении в таблицу. Указывать его в списке несущих полей не надо.

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

r    Для каждой таблицы можно хранить один дополнительный блок информации любой структуры (например, это может быть даже многомерный ассоциативный массив). Выборка и запись этого блока осуществляются методами GetInfo() и SetInfo(). Блок информации нельзя получить никак иначе, кроме как посредством этих двух функций (он "не виден" даже для функции выборки).

r    Для убыстрения работы программист может назначить для тех или иных столбцов таблицы режим индексирования (при использовании индекса MySQL тратит значительно меньше времени на поиск данных). Индексы, как и несущие поля, вставляются и удаляются автоматически при изменении параметров вызова конструктора. Помните, что хотя они и убыстряют работу, но зато занимают на диске довольно много места.

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

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

в защите, рекомендуется перед вставкой записи в таблицу проверять, какой объем она занимает в сериализованном виде, и в случае превышения определенного числа байтов выводить предупреждение и завершать сценарий по die(). Думаю, читатель сам без труда добавит такую возможность в свои сценарии или же прямо в класс MysqlTable.



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

Листинг 31.2. Полноценный класс MySQL-таблицы

<?

// MysqlTable — "прозрачная работа" с таблицей MySQL.

// Класс MysqlTable обычно делают базовым для какого-нибудь

// другого класса (например, CGuestBook), и переопределяют

// нужные функции.

// Поле для хранения сериализованных полей (снаружи "не видно")

define("DataField","__data__");

//******************* Вспомогательные функции *******************

// Если переменная пуста, инициализирует ее

function Def0(&$st,$def) { if(!isSet($st)||$st=="") $st=$def; }

// Подготавливает строку двоичных данных для помещения в таблицу.

function Apostrophs(&$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($s); }

// Распаковывает строку и создает объект.

function SqlUnpack(&$st) { return Unserialize($st); }

//****************************************************************

//*** Далее идет описание класса таблицы.

// Каждая запись таблицы, помимо тех полей, которые указаны в

// конструкторе, будет иметь еще два поля — 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(!isSet($this->Fields[$k])) $Data[$k]=$v;

 return Serialize($Data);

}

// Виртуальная функция производного класса вызывается ПЕРЕД любым

// занесением данных в таблицу (добавлением и обновлением). То есть

// она предназначена для "прозрачной" автоматической генерации некоторых

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

// перед ее сохранением.

// Можно, к примеру, в таблице держать какую-нибудь дату в формате

// SDN, а "делать вид", что она хранится в обычном представлении

// "дд.мм.гггг".

// Если эта функция возвратит 0, то операция закончится с ошибкой.

function PreModify(&$Rec) { return 1; }

// Виртуальная функция вызывается ПОСЛЕ выборки записи из таблицы, а

// также в конце модификации записи. То есть она предназначена для

// "прозрачной" модификации только что полученной из таблицы записи.

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

// из таблицы STM-поле преобразовать в "дд.мм.гггг", и "никто ничего

// не заметит".

function PostSelect(&$Rec) { return; }

// Возвращает имя таблицы

function GetTableName() { return $this->TableName; }

// Возвращает результат запроса select. В дальнейшем этот результат

// (дескриптор) будет, скорее всего, обработан при помощи GetResult().

// $Expr — выражение SQL, по которому будет идти выборка

// $Order — правила сортировки (по умолчанию — по убыванию id)



function TableSelectQuery($Expr="",$Order="id desc")

{ $this->Error="";

 if(!$Expr) $Expr="1=1";

 $r=mysql_query("select * from ".$this->TableName.

 " where ($Expr) and (id>1) order by $Order");

 if(!$r) { $this->Error=mysql_error(); return; }

 return $r;

}

function SelectQuery($Expr="",$Order="id desc")

{ return $this->TableSelectQuery($Expr,$Order); }

// Возвращает результат предыдущего запроса select (точнее, очередную

// найденную запись) в виде распакованного (!) массива. Если

// SelectQuery() нашла несколько записей, то, последовательно вызывая

// GetResult(), можно считать их все. Метод делает всю "черную" работу

// по сериализации. Еще раз: если у результата несколько строк, то метод

// возвращает очередную. Если строки кончились, возвращает "".

// Чаще всего в вызове этой функции (и функции SelectQuery) нет

// необходимости — можно воспользоваться методом Select(), который по

// запросу сразу возвращает массив со всеми обработанными результатами!

function TableGetResult($r)

{ $this->Error="";

 // Выбираем очередную строку в виде массива

 if($r) $Result=mysql_fetch_array($r);

 else $this->Error=mysql_error();

 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, даже если они

// переопределены в нем. К сожалению, в PHP это единственный метод

// добиться цели.



// Аналог mysql_num_rows()

function GetNumRows($r) { return mysql_num_rows($r); }

// Аналог mysql_data_seek(). После вызова этой функции указатель на

// дескриптор $r "перескочит" на найденную запись номер $to, после

// чего GetResult() ее и возвратит.

function DataSeek($r,$to) { return mysql_data_seek($r,$to); }

 

// Создает или загружает таблицу по имени $Name.

// $Fields — список полей базы. Именно по ним в дальнейшем можно

// будет вести поиск и строить индекс. Кроме того, в запись можно будет

// добавлять ЛЮБЫЕ другие переменные, но они будут сериализованы, а

// потом восстановлены. Формат списка: массив с ключами — именами

// переменных и значениями — их типами. Если $Fields — не массив, то

// считается, что таблица открывается такой, какой она есть. В противном

// случае производится проверка: не добавились или не удалились ли какие-

// то поля или индексы и, если это так, то выполняется соответствующая

// модификация таблицы (кстати, это процесс довольно длительный).

// ВНИМАНИЕ: если в таблице было какое-то поле, которое сериализуется, то

// в будущем при добавлении этого поля к $Fields оно НЕ будет

// автоматически переведено в ранг несущих, т. е.

попросту

// пропадет (и наоборот).

// РЕКОМЕНДАЦИЯ: перечисляйте в $Fields те поля, для которых вы ТОЧНО

// уверены, что они будут всегда присутствовать в базе, а также те,

// по которым нужно будет вести поиск, строить индекс и использовать

// distinct.

// $Index — по каким полям нужно строить индекс. Индекс несколько

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

// (точнее, по тем полям, для которых используется индекс). Ключи — имена

// столбцов, значения — "размер" индекса (0, если по умолчанию, что чаще

// всего наиболее разумно)

function MysqlTable($Name,$Fields="",$Index="")

{ $this->TableName=$Name; $this->Error="";

 if(is_array($Fields)) {

 foreach($Fields as $k=>$v)



 if(!eregi("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=1")

 );

 // Если таблица существует, то запрос окончится успешно.

 // В этом случае нужно проверить, не изменилась ли таблица с момента

 // последнего обращения, и если это так, то подкорректировать ее.

 if(@is_array($Data)) {

 if(!is_array($Fields)) {

 $this->Error="Couldn't create table: no fields specified";

 return;

 }

 Def0($Data["Fields"],array());

 Def0($Data["Index"],array());

 //** Возможно, что-то изменилось. Тогда выполняем alter table.

 //1. Добавились поля?

 $Lst=array();

 foreach($Fields as $k=>$v) {

         if(!isSet($Data["Fields"][$k])) $Lst[]="add $k $v";

 else if($Data["Fields"][$k]!=$v) $Lst[]="change $k $k $v";

 }

 //2. Удалились поля?

 foreach($Data["Fields"] as $k=>$v)

 if(!isSet($Fields[$k])) $Lst[]="drop $k";

 //3. Добавились индексы?

 foreach($Index as $k=>$v) if(!isSet($Data["Index"][$k]))

 $Lst[]="add index index_$k ($k".($v!=0?" ($v)":"").")";

 //4. Удалились индексы?

 foreach($Data["Index"] as $k=>$v)

 if(!isSet($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=1;

 }

 $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=1;

 }

 // Сохраняем информацию о таблице, если она поменялась

 if(!empty($Changed)||$this->JustCreated) {

 $Data["Fields"]=$Fields;

 $Data["Index"]=$Index;

 Def0($Data["Info"],array()); // Информации не было — делаем пустой

 $Data=SqlPack($Data);

 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=1");

 }

 if(!$Result) { $this->Error=mysql_error(); return; }

 }

 $this->Fields=$Fields;

 $this->Index=$Index;

}

 

// Записывает в таблицу информацию, общую для всей таблицы. Эта

// информация может быть получена потом только при помощи метода

// GetInfo(), и никак иначе. Например, если таблица используется для

// гостевой книги, мы можем сюда записывать какие-нибудь параметры этой

// книги — скажем, имя и пароль владельца. $Inf может быть чем угодно —

// даже массивом.

function TableSetInfo($Inf)

{ $this->Error="";

 // Читаем информационную запись

 $r=mysql_query("select ".DataField." from ".

         $this->TableName." where id=1");

 if(!($Data=$this->GetResult($r))) return;

 // Устанавливаем поле Info

 $Data["Info"]=$Inf;

 $Data=SqlPack($Data);

 // Сохраняем результат

 if(!mysql_query("update ".$this->TableName.

 " set ".DataField."='$Data' where id=1"))

 { $this->Error=mysql_error(); return; }

 return 1;



}

function SetInfo($Inf) { return $this->TableSetInfo(&$Inf); }

// Возвращает информацию о таблице, ранее занесенную в нее при помощи

// SetInfo. Если информация не была занесена, возвращает пустой массив.

function TableGetInfo()

{ $this->Error="";

 // Читаем информационную запись

 $r=mysql_query("select * from ".$this->TableName." where id=1");

 // Если что-то не в порядке, GetResult установит поле Error у объекта

 if(!($Data=$this->GetResult($r))) return array();

 if(!@is_array($Data["Info"])) $Data["Info"]=array();

 return $Data["Info"];

}

function GetInfo() { return $this->TableGetInfo(); }

// Уничтожает таблицу. Осторожно! Таблица удаляется без всяких

// предупреждений!!!

function TableDrop()

{ $this->Error="";

 if(!mysql_query("drop table ".$this->TableName)) {

 $this->Error=mysql_error();

         return 0;

 }

 return 1;

}

function Drop() { 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])."'";

 }

 $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(&$Rec); }

 

// Удаляет из таблицы записи, удовлетворяющие выражению $Expr.

// Например: $Tbl->Delete("(id=$id) or (id=0)");

function TableDelete($Expr)

{ $this->Error="";

 if(!mysql_query("delete from ".$this->TableName.

 " where ($Expr) and (id>1)"))

 { $this->Error=mysql_error(); return 0; }

 return 1;

}

function Delete($Expr) { return $this->TableDelete($Expr); }

 

// Возвращает массив записей (ключ — id, значение — запись). В массив

// будет занесено не более $Num записей. Для каждой записи

// вызывается PostSelect()!

function TableSelect($Expr="",$Num=100000,$Order="id desc")

{ $this->Error="";

 // Выполнить запрос

 $r=$this->SelectQuery($Expr,$Order); if(!$r) return 0;

 // Цикл по найденным записям

 for($i=0,$Found=array(); $i<$Num&&($Rec=$this->GetResult($r)); $i++)

 $Found[$Rec["id"]]=$Rec;

 return $Found;

}

function Select($Expr="",$Num=100000,$Order="id desc")

{ return $this->TableSelect($Expr,$Num,$Order); }

// Обновляет запись в таблице, при этом запись $Upd изменяется и

// становится фактически такой, как она будет выглядеть после обновления.

// То есть к ней могут добавиться новые поля из таблицы. Если записи с

// таким id нет (когда $id не указан в параметрах, его значение берется

// равным $Upd["id"]), то генерируется ошибка!

// Возможно, в записи $Upd не задан идентификатор id (это бывает, если

// мы только что получили данные из формы). В этом случае можно этот

// идентификатор передать через $id.

// Итак, при обновлении id НЕ МЕНЯЕТСЯ по определению (в отличие от

// ДОБАВЛЕНИЯ, когда 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,",");

 // Выполняем запрос

 if(!mysql_query("update ".$this->TableName.

 " set $Lst where id=".$Rec["id"]))

 { $this->Error=mysql_error(); return 0; }

 $this->PostSelect($Rec);

 return 1;

}

function Update(&$Upd,$id=0) { return $this->TableUpdate(&$Upd,$id); }

// Возвращает число записей, удовлетворяющих выражению $Expr.

// Если $Expr не задано, возвращает число ВСЕХ записей.

function TableGetCount($Expr="")

{ $this->Error="";

 if(!$Expr) $Expr="1=1";

 $r=mysql_query("select count(if(($Expr) and (id>1),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

// в таблице, удовлетворяющих тому же условию $Expr.

// ВНИМАНИЕ: эта функция работает лишь тогда, когда поле $field

// присутствовало среди полей $Fields при вызове конструктора.

// В противном случае генерируется ошибка.

// Рекомендуется при создании таблицы для поля $field создать индекс.



function TableGetDistinct($field,$Expr="")

{ $this->Error="";

 if(!$Expr) $Expr="1=1";

 $r=mysql_query("select distinct $field from ".

 $this->TableName." where ($Expr) and (id>1)");

 // distinct НЕ работает вместе с order by! Почему — неясно...

 if(!$r) { $this->Error=mysql_error(); return 0; }

 for($Arr=array(),$i=0,$n=mysql_num_rows($r); $i<$n; $i++)

 $Arr[]=mysql_result($r,$i,0);

 return $Arr;

}

function GetDistinct($field,$Expr="")

{ return $this->TableGetDistinct($field,$Expr); }

}; // Конец класса

?>

А вот пример применения этого класса (листинг 31.3). Делает он следующее: открывает таблицу в некоторой базе данных (если таблицы с таким именем не существует, создает ее) и добавляет одну пробную запись.

Листинг 31.3. Пример использования класса MysqlTable

<?

include "librarian.phl"; // подключаем библиотекарь

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->GetInfo();

$Inf["a"]=@$Inf["a"]+1;

$Inf["b"]=@$Inf["b"]+10;

echo $Inf["a"]," ",$Inf["b"],"<br>";

$t->SetInfo($Inf);

// Выбираем все записи и выводим их

$d=$t->Select();

foreach($d as $id=>$Data) {

 echo "$id: ".$Data['t']."<br>";

}

?>

Попробуйте запустить этот сценарий (естественно, сделав так, чтобы ему был доступен библиотекарь), а затем понажимать кнопку Обновить

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


Положение указателя текущей позиции


int feof(int $f)

Возвращает true, если достигнут конец файла (то есть если указатель файла установлен за концом файла). Эта функция чаще всего используется в следующем контексте:

$f=fopen("myfile.txt","r");

while(!feof($f))

{  $st=fgets($f);

   // теперь мы обрабатываем очередную строку $st

   // . . .

}

fclose($f);

Лучше избегать подобных конструкций, т.к. в случае больших файлов они довольно медлительны. Лучше читайте файл целиком при помощи File() (см. ниже) или fread() — конечно, если вам нужен доступ к каждой строке этого файла, а не только к нескольким первым!

int fseek(int $f, in $offset, int $whence=SEEK_SET)

Устанавливает указатель файла на байт со смещением $offset (от начала файла, от его конца или от текущей позиции, в зависимости от параметра $whence). Это, впрочем, может и не сработать, если дескриптор $f ассоциирован не с обычным локальным файлом, а с соединением HTTP или FTP.

Параметр $whence, как уже упоминалось, задает, с какого места отсчитывается смещение $offset. В PHP для этого существуют три константы, равные, соответственно, 0, 1 и 2:

r    SEEK_SET — устанавливает позицию начиная с начала файла;

r    SEEK_CUR — отсчитывает позицию относительно текущей позиции;

r    SEEK_END — отсчитывает позицию относительно конца файла.

В случае использования последних двух констант параметр $offset вполне может быть отрицательным (а при применении SEEK_END он будет отрицательным наверняка).

Как это ни странно, но в случае успешного завершения эта функция возвращает 0, а в случае неудачи -1. Почему так сделано — неясно. Наверное, по аналогии с ее Си-эквивалентом?

int ftell(int $f)

Возвращает положение указателя файла. Собственно, вот и все, что делает эта функция.



Получение ближайшего цвета


Давайте разберемся, зачем это придумана такая технология [E96][DK97] работы с цветами через промежуточное звено — идентификатор цвета. А дело все в том, что некоторые форматы изображений (такие как GIF и частично PNG) не поддерживают любое количество различных цветов в изображении. А именно, в GIF количество одновременно присутствующих цветов ограничено цифрой 256, причем чем меньше цветов используется в рисунке, тем лучше он "сжимается"

и тем меньший размер имеет файл. Тот набор цветов, который реально использован в рисунке, называется его палитрой.

Представим себе, что произойдет, если все 256 цветов уже "заняты"

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

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

int imageColorClosest(int $im, int $red, int $green, int $blue)

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

на запрос желтого цвета будет, скорее всего, возвращен идентификатор зеленого — он "ближе всего"

с точки зрения GD соответствует понятию "зеленый".



Получение целой строки результата


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

array mysql_fetch_row(int $result)

Функция возвращает массив-список со значениями полей очередной строки результата $result. Если указатель текущей позиции результата был установлен за последней записью (то есть строки кончились), возвращает false. Текущая позиция сдвигается к следующей записи, так что очередной вызов mysql_fetch_row() вернет следующую строку результата.

Эту функцию чаще всего применяют в таком контексте:

$r=mysql_query("select * frim OurTable where age<30");

while($Row=mysql_fetch_row($r)) {

  // îáðàáàòûâàåì ñòðîêó $Row

}

Как видим, цикл оборвется, как только строки закончатся, т. е. когда mysql_fetch_row() вернет false.

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

array mysql_fetch_array(int $result)

Функция mysql_fetch_array() возвращает очередную [E140] [DK141] строку результата в

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

ключами.

Может возникнуть вопрос: зачем вообще тут нужны числовые индексы. Ответ прост: дело в том, что в результате выборки в действительности [E142] [DK143] могут присутствовать поля (фактически, колонки) с одинаковыми именами, но, соответственно, с различными индексами. Это происходит тогда, когда выборка в SELECT

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

Рекомендую всегда вместо mysql_fetch_row() использовать функцию mysql_fetch_array(), потому что она более универсальна и к тому же, как написано в документации, не намного медленнее.

int mysql_data_seek(int $result, int $row_number)

Эта функция устанавливает указатель текущей строки в результате $result в позицию $row_number, так что следующий вызов mysql_fetch_row() и mysql_fetch_array() вернет значения полей именно этой строки. Возвращает false в случае ошибки

или если строки кончились.



Получение части массива


array array_slice(array $Arr, int $offset [, int $len])

Эта функция возвращает часть массива ассоциативного массива, начиная с пары ключ=>значения со смещением (номером) $offset от начала и длиной $len (если последний параметр не задан, до конца массива).

Параметры $offset

и $len

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

и т.д. Вот несколько примеров из документации PHP:

$input = array ("a", "b", "c", "d", "e");

$output = array_slice ($input, 2);      // "c", "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"



Получение числа записей, удовлетворяющих выражению


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

select count(if(Âûðàæåíèå,1,NULL)) from Òàáëèöà

Уже этот пример показывает, насколько богаче[E135] [DK136]  язык MySQL по сравнению с тем, что было описано...



Получение Cookie


Еще кое-что о Cookies. Предположим, сценарий отработал и установил какой-то Cookie, например, с именем Cook и значением Val. В следующий раз при запуске этого сценария (на самом деле, и всех других сценариев, расположенных на том же сервере в том же каталоге или ниже по дереву) ему передастся пара типа Cook=Val (через специальную переменную окружения). PHP это событие перехватит и автоматически создаст переменную $Cook со значением Val. То есть интерпретатор действует точно так же, как если бы значение нашего Cookie пришло откуда-то из формы. Та переменная, которую мы установили в прошлый раз, будет доступна и сейчас!



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


Получить Cookies для сценария несколько проще: все они хранятся в переменной окружения HTTP_COOKIE в таком же формате, как и QUERY_STRING, только вместо & используется ;. Например, если мы установили два Cookies: cookie1=value1 и cookie2=value2, то в переменной окружения HTTP_COOKIE будет следующее:

cookie1=value1;cookie2=value2.

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



Получение информации о результате


Будет полезно рассмотреть еще несколько функций, предназначенных для получения различной информации о результате запроса.

string mysql_field_name(int $result, int $field_index)

Функция mysql_field_name() возвращает имя поля, которое расположено в результате по смещению $field_index. В общем-то, применяется довольно редко, что связано с существованием функции mysql_fetch_array().

Итак, с помощью функции mysql_field_name()

мы можем "переводить" числовые X-координаты в двумерном массиве результата в их ассоциативные эквиваленты.

string mysql_field_type(int $result, int $field_offset)

Эта функция похожа на mysql_field_name(), только возвращает не имя, а тип соответствующей колонки в результате. Им может быть, например, int, double и т.д.

int mysql_field_len(int $result, int $field_offset)

Функция возвращает длину поля в результате $result. Поле, как обычно, задается указанием его смещения. Под длиной здесь подразумевается не размер данных поля в байтах, а тот размер, который был указан при его создании. Например, если поле имеет тип varchar и было создано (вместе с таблицей) с типом varchar(100), то для него будет возвращено 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

По этому полю построен индекс

Blob

Поле может содержать бинарный блок данных, который никак не интерпретируется

<
Таблица 26.7 (окончание)

Флаг

Описание

Unsigned

Поле содержит беззнаковые числа

zerofill

Использовать символы с нулевым кодом вместо пробелов

binary

Поле содержит бинарные данные

enum

Поле содержит элемент перечисления, т. е. только один элемент из нескольких возможных

auto_increment

Это поле автоматически нумеруется. Проставляется MySQLпри добавлении новой записи так, чтобы в таблице никогда не образовывалось нескольких строк с одинаковым значением этого поля

timestamp

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

string mysql_field_table(int $result, int $field_offset)

Возвращает имя таблицы, из которой было извлечено поле со смещением $field_offset в результате $result. Как уже говорилось, результат запроса может быть получен из нескольких таблиц, так что вот вам средство для извлечения имени таблицы по номеру поля.


Получение поля результата


int mysql_result(int $result, int $row, mixed $field)

Функция возвращает значение поля $field в строке результата с номером $row. Параметр $field может задавать не только имя поля, но и его номер— позицию, на которой столбец "стоял"

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

Если мы опять будем рассматривать результат как двумерный массив полей, то параметр $field надо поставить в соответствие его X-координате, а $row — Y-координате. В этом понимании X-координата чаще всего будет ассоциативной — т. е. в ней задается не число, а имя столбца.

Функция универсальна: с ее помощью можно "обойти"

весь результат по одной ячейке. И хотя это не возбраняется, но делать, однако, не рекомендуется, т. к. mysql_result() работает довольно медленно. Лучше воспользоваться функциями, которые описываются дальше.



Получение RGB-составляющих


array imageColorsForIndex(int $im, int $index)

Функция возвращает ассоциативный массив с ключами red, green и blue (именно в таком порядке), которым соответствуют значения, равные величинам компонент RGB в идентификаторе цвета $index. Впрочем, мы можем и не обращать особого внимания на ключи и преобразовать возвращенное значение как список:

$c=imageColorAt($i,0,0);

list($r,$g,$b)=array_values(imageColorsForIndex($i,$c));

echo "R=$r, g=$g, b=$b";

Эта функция ведет себя противоположно по отношению к imageCollorAllocate() или imageColorClosest().



Получение уникальных значений столбцов


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

значения существуют в данном столбце таблицы. Например, если у каждой записи в некоей статистической таблице, содержащей сведения о людях, у нас есть поле Country (страна), в котором указана страна проживания конкретного человека, и мы хотим выяснить, в каких же странах проживают все люди, дожившие до[DK137]  30 лет, занесенные в таблицу, можно выполнить запрос:

select distinct ÈìÿÏîëÿ from Òàáëèöà where Âûðàæåíèå

В нашем случае ИмяПоля=Country, а Выражение — что-то вроде age>=30. Этот запрос сгенерирует результат, состоящий из одного столбца, в котором и будут перечислены искомые страны.



Порт


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

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

программа, запущенная где-то на компьютере в Интернете. Именно между процессами, а не между машинами, и осуществляется обмен данными в терминах протокола TCP. Мы уже знаем, как идентифицируются отдельные компьютеры в Сети. Осталось рассмотреть, как же TCP определяет тот процесс, которому нужно доставить данные.

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

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

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

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


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




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



POST


r    Формат: POST сценарий?параметры HTTP/1.0

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

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



Построчные чтение/запись


string fgets(int $f, int $length)

Читает из файла одну строку, заканчивающуюся символом новой строки \n. Этот символ также считывается и включается в результат. Если строка в файле занимает больше $length-1 байтов, то возвращаются только ее $length-1 символов. Функция полезна, если вы открыли файл и хотите "пройтись"

по всем его строкам. Однако даже в этом случае лучше (и быстрее) будет воспользоваться функцией File(), которая рассматривается ниже. Стоит также заметить, что эта функция (ровно как и функция fread()) в случае текстового режима) в Windows заботится о преобразовании пар \r\n в один символ \n, так что будьте внимательны при работе с текстовыми файлами в этой операционной системе.

int fputs(int $f, string $st)

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



Посылка писем через PHP


Одно из самых мощных средств PHP — возможность автоматической посылки писем по электронной почте, минуя использование внешних программ и утилит. Функция отсылки встроена в PHP. С нее мы и начнем.



Посылка в указанной кодировке


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

$message=

"From: Лист рассылки

To: Иванов Иван Иванович

Subject: Пробная рассылка

Content-type: text/plain; charset=windows-1825

Уважаемый товарищ! Это письмо послано почтовым роботом.

Всего хорошего!";

Mail("ivanov@ivan.ivanovich.ru","",$message);

Обратите внимание на заголовок Content-type (в некоторых системах он обязательно должен стоять последним — проверьте это экспериментально). Он задает, что, во-первых, письмо доставляется как простой текст (text/plain), а во-вторых, что его кодировка — Windows. Теперь письмо всегда можно будет прочитать, даже если почтовая программа клиента по умолчанию настроена на китайскую кодировку.

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

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



Pragma


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

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

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



с бурным развитием сети Интернет


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


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

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

Предопределенные константы


Константы бывают двух типов: одни — предопределенные (то есть устанавливаемые самим интерпретатором), а другие определяются программистом. Существуют несколько предопределенных констант.

r    __FILE__

Хранит имя файла программы, которая выполняется в данный момент.

r    __LINE__

Содержит текущий номер строки, которую обрабатывает в текущий момент интерпретатор. Эта своеобразная "константа" каждый раз меняется по ходу исполнения программы.

r    PHP_VERSION

Версия интерпретатора PHP.

r    PHP_OS

Имя операционной системы, под которой работает PHP.

r    TRUE или true

Эта константа нам уже знакома и содержит значение "истина".

r    FALSE или false

Содержит значение "ложь".



Представление времени в формате timestamp


int time()

Возвращает время в секундах, прошедшее с полуночи 1 января 1970 года по Гринвичу до настоящего момента. Этот формат данных принят в Unix как стандартный: в частности, время последнего изменения файлов указывается именно в таком формате (как вы, возможно, помните по описанию функции file_mtime()). Вообще говоря, почти все функции по работе со временем имеют дело именно с таким его представлением (которое называется timestamp). То есть представление "количество секунд с 1 января 1970 года весьма универсально и, что главное, — удобно.

На самом-то деле, timestamp не отражает реальное (астрономическое) число секунд с 1 января 1970 года, а немного отличается от него. Впрочем, это нисколько не умаляет преимущества от его использования.

string microtime()

Возвращает строку в формате: "микросекунды секунды[E75] ", где секунды — то, что возвращается функцией time(), а микро[E76] секунды — дробная часть секунд, служащая для более точного измерения промежутков времени. Эта функция работает только в системах, которые поддерживают системный вызов gettimeofday(), т. е. практически во всех.

С функцией microtime()

связано одно недоразумение. Дело в том, что миллисекунды в различных ОС выглядят по-разному. Например, в Unix это действительно число микро[E77] секунд, а в Windows — непонятное значение, связанное неизвестно с чем. Возможно, оно все же несет какой-то смысл, но мне до него "докопаться", увы, не удалось.

int mktime([int $hour] [,int $minute] [,int $second] [,int $month]

           [,int $day] [,int $year])

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

Правильность даты, переданной в параметрах, не проверяется. В случае некорректной даты ничего особенного не происходит — функция "делает вид", что это ее не касается[E78] , и формирует соответствующий timestamp. Для иллюстрации рассмотрим три вызова (два из них — с ошибочной датой), которые тем не менее возвращают один

и тот же результат:

echo date("M-d-Y", mktime(0,0,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));  // íåïðàâèëüíàÿ äàòà

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



Преобразование адресов E-mail


Задача: имеется текст, в котором иногда встречаются строки вида

пользователь@хост, т. е. E-mail-адреса в обычном формате (или хотя бы большинство таких E-mail[E88] ). Необходимо преобразовать их в HTML-ссылки.

Решение:

$text=eregi_Replace(

  '([[:alnum:]-.]+@'.                    // пользователь

    '[[:alnum:]-]+(\\.[[:alnum:]-]+)*'.  // домен

      '(\\?([[:alnum:]?+&%]*)?)?'.       // необязательные параметры

  ')',

  '<a href="\\1">\\1</a>',

  $text

);

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



Преобразование гиперссылок


Задача: имеется текст, в котором иногда встречаются подстроки вида

протокол://URL, где протокол— один из протоколов http, ftp или gopher, а URL — какой-нибудь адрес в Интернете. Нужно заместить их на HTML-эк­виваленты <a href=…>…</a>.

Решение:

$w="[:alnum:]";

$p="[:punct:]";

$text=eregi_Replace(

  "((https?|ftp|gopher)://".          // протокол

    "[$w-]+(\\.[$w-]+)*".             // имя хоста

      "(/[$w+&.%]*(\\?[$w?+&%]*)?)?". // имя файла и параметры

  ")",

  '<a href="\\1">\\1</a>',

  $text

);



Преобразование кодировок


Часто встречается ситуация, когда нам требуется преобразовать строку из одной кодировки кириллицы в другую. Например, мы в программе сменили локаль: была кодировка windows, а стала— KOI8-R. Но строки-то остались по-прежнему в кодировке WIN-1251[E54] , а значит, для правильной работы с ними нам нужно их перекодировать в KOI8-R. Для этого и служит функция преобразования кодировок.

string convert_cyr_string(string $str, char $from, char $to);

Функция переводит строку $str из кодировки $from в кодировку $to. Конечно, это имеет смысл только для строк, содержащих "русские" буквы, т. к. латиница во всех кодировках выглядит одинаково. Разумеется, кодировка $from должна совпадать с истинной кодировкой строки, иначе результат получится неверным. Значения $from и $to — один символ, определяющий кодировку:

r    k — koi8-r

r    w — windows-1251

r    i — iso8859-5

r    a — x-cp866

r    d — x-cp866

r    m — x-mac-cyrillic

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



с примера сценария, который представляет


Начнем сразу с примера сценария, который представляет собой не HTML-страницу в обычном смысле, а рисунок PNG. То есть URL этого сценария можно поместить в тэг:
<img src=button.php?Hello+world!>
Как только будет загружена страница, содержащая указанный тэг, сценарий запустится и отобразит надпись Hello world! на фоне рисунка, лежащего в images/button.png. Полученная картинка нигде не будет храниться— она создается "на лету".




Рис. 23.1. Демонстрация

возможностей вывода

TrueType-шрифтов на PHP
Листинг 23.1. Создание картинки "на лету"
<?
// Получаем строку, которую нам передали в параметрах
$string=$QUERY_STRING;
// Загружаем рисунок фона с диска
$im = imageCreateFromPng("images/button.png");
// Создаем в палитре новый цвет — оранжевый
$orange = imageColorAllocate($im, 220, 210, 60);
// Вычисляем размеры текста, который будет выведен
$px = (imageSx($im)-7.5*strlen($string))/2;
// Выводим строку поверх того, что было в загруженном изображении
imageString($im,3,$px,9,$string,$orange);
// Сообщаем о том, что далее следует рисунок PNG
Header("Content-type: image/png");
// Теперь — самое главное: отправляем данные картинки в
// стандартный выходной поток, т. е. в браузер
imagePng($im);
// В конце освобождаем память, занятую картинкой
imageDestroy($im);
?>
Итак, мы получили возможность "на лету"
создавать стандартные кнопки с разными надписями, имея только "шаблон"
кнопки.


В листинге 23.3 я привожу пример сценария, который использует возможности вывода TrueType-øрифтов, а также демонстрирует работу с цветом RGB. Хотя размер примера довольно велик, рисунок, который он генерирует, выглядит довольно привлекательно
(см. рис. 23.1)[E115][DK116] .
Листинг 23.3. Вывод строки произвольного формата
<?
// Аналог imageColorAllocate() (по умолчанию), но работает не с
// RGB-тройкой, а с цветом в формате XXYYZZ, где:
// * XX — red-составляющая в шестнадцатеричном формате;
// * YY — green-составляющая в шестнадцатеричном формате;
// * ZZ — blue-составляющая в шестнадцатеричном формате.
// Можно указать другую функцию получения цвета, задав ее
// имя в параметре $func (например, imageColorClosest).
function imageColorHex($im, $c, $func="imageColorAllocate")
{ // Сначала дополняем нулями в начале, если нужно
  for($i=strlen($c); $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)
if(!@$a) $a=30;       // угол поворота (по умолчанию 30)
if(!@$s) $s=80;       // размер шрифта (80)
if(!@$b) $b="00AAAA"; // цвет заднего плана (зеленовато-голубой)
if(!@$c) $c="FFFF00"; // цвет букв (ярко-желтый)
if(!@$d) $d=10;       // зазор между текстом и границей рисунка
if(!@$f) $f="times";  // шрифт
if(!@$text) $text="Hello world!"; // текст
// Получаем границы рамки текста
$Bnd=imageTTFBBox($s,$a,getcwd()."/$f.ttf",$text);
// Массивы x- и y-координат всех точек
$X=$Y=array();
// Заполняем эти массивы на основании $Bnd
for($i=0; $i<4; $i++) {
  $X[]=$Bnd[$i*2];
  $Y[]=$Bnd[$i*2+1];
}
// Вычисляем размер картинки с учетом зазора $d
$MX=max($X)-min($X)+$d*2; // размер по x
$MY=max($Y)-min($Y)+$d*2; // размер по y


Напоследок рассмотрим пример применения описанных выше функций. Предположим, в некотором текстовом файле хранится список подписчиков, каждая строка которого оформлена в следующем формате:
Имя_подписчика|адрес|timestamp_подписки|кодировка_письма
Напишем сценарий, который будет посылать каждому подписчику из этой простейшей базы данных "личное" письмо с самыми последними новостями сайта. Предположим для простоты, что эти новости в программе уже сохранены в массиве $News.
Для начала создадим шаблон письма (листинг32.3):
Листинг 32.3. Шаблон "личного" письма: mail.txt
Content-type: text/plain
From: Система рассылки <subscribe@ourserver.ru>
To: <?=$User['name']?>.
Subject: Свежие новости
Content-type: text/plain
~StartOfMail
Уважаемый <?=$User['name']?>!
Вы подписались на наш лист рассылки <?=date("d.m.Y",$User['time'])?>.
Предлагаем Вашему вниманию последние новости.
---------------------------------------------------------------
<?foreach($News as $k=>$v) {?>
<?=WordWrap($v,60)?>.
<?}?>
Как видим, шаблон практически ничем не отличается от небольшого сценария на PHP. Он получает данные из переменных $User (данные пользователя) и $News (блоки новостей), которые должны устанавливаться запускающей программой. Вскоре мы рассмотрим процедуру более подробно, а пока обратите внимание на некоторые моменты при написании этого шаблона.
r    Мы указали заголовок Content-type сразу в двух местах шаблона — в начале и конце. В силу рассуждений, приведенных в главе 20, это необходимо для того, чтобы помочь некоторым "недогадливым" почтовым программам в определении кодировки письма.
r    Заметьте, что в конце заголовка To стоит точка.  Зачем она нужна? Дело в том, что закрывающий тэг PHP ?>, если он занимает последние символы строки, никогда не генерирует знака перевода строки \n. Это, видимо, сделано для того, чтобы уменьшить количество пустых строк в страницах, которые создает интерпретатор. В нашем случае отсутствие разделителя может сильно помешать,


// Теперь вычисляем координаты базовой точки строки, чтобы
// она располагалась точно по центру поля картинки
$x=$d+$Bnd[0]-min($X)+2;
$y=$d+$Bnd[1]-min($Y)+2;
// Создаем рисунок нужного размера
$im = imageCreate($MX,$MY);
// Создаем в палитре новые цвета
$black = imageColorHex($im, 0);  // черный (тень)
$back  = imageColorHex($im, $b); // задний план
$front = imageColorHex($im, $c); // цвет букв
// Очищаем задний план
imageFill($im,0,0,$back);
imageRectangle($im,0,0,$MX-1,$MY-1,$black);
// Выводим тень от текста
imagettftext($im,$s,$a,$x+2,$y+2,$black,getcwd()."/$f.ttf",$text);
// Выводим текст
imagettftext($im,$s,$a,$x,$y,$front,getcwd()."/$f.ttf",$text);
// Выводим рисунок в браузер
Header("Content-type: image/png");
imagePng($im);
?>
Сценарий из листинга 23.3 (назовем его ttf.php)
генерирует картинку с заданным цветом заднего плана, в которую выводится указанная строка с тенью. При этом используется TrueType-шрифт, а также определяются размер строки, угол ее наклона, цвет и т. д.
Формат вызова сценария имеет следующий общий вид:
ttf.php?a=Градусы&s=Размер&b=ЗаднийЦвет&c=Цвет&d=Зазор&f=Фонт&text=Текст
Ни один из этих параметров не является обязательным — в случае пропуска подставляются значения по умолчанию (см. листинг 23.3).
Необходимо заметить, что прежде, чем запускать сценарий, нужно скопировать TTF-файл со шрифтом в каталог, где расположена программа (например, взяв его из C:/WINDOWS/FONTS для платформы Windows). Параметр f задает имя этого файла без расширения, и ищется он в текущем каталоге. По умолчанию выбран шрифт Times.



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


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

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

#include <time.h>   // Íóæíà äëÿ èíèöèàëèçàöèè ôóíêöèè rand()

#include <stdio.h>  // Âêëþ÷àåì ïîääåðæêó ôóíêöèé ââîäà/âûâîäà

#include <stdlib.h> // À ýòî— äëÿ ïîääåðæêè ôóíêöèè rand()

// Ãëàâíàÿ ôóíêöèÿ. Èìåííî îíà è çàïóñêàåòñÿ ïðè ñòàðòå ñöåíàðèÿ.

void main(void) {

  // èíèöèàëèçèðóåì ãåíåðàòîð ñëó÷àéíûõ ÷èñåë

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

  // â Num çàïèñûâàåòñÿ ñëó÷àéíîå ÷èñëî îò 0 äî 9

  Num = rand()%10;

  // äàëåå âûâîäèì çàãîëîâêè îòâåòà. Òèï — html-äîêóìåíò


  printf("Content-type: text/html\n");
  // çàïðåò êýøèðîâàíèÿ
  printf("Pragma: no-cache\n");
  // ïóñòîé çàãîëîâîê
  printf("\n");
  // âûâîäèì òåêñò äîêóìåíòà — åãî ìû óâèäèì â áðàóçåðå
  printf("<html><body>");
  printf("<h1>Çäðàâñòâóéòå!</h1>");
  printf("Ñëó÷àéíîå ÷èñëî â äèàïàçîíå 0-9: %d",Num);
  printf("</body></html>");
}
Исходный текст можно откомпилировать и поместить в каталог с CGI- сценариями на сервере. Обычно стараются все сценарии хранить в одном месте — в каталоге cgi-bin, у которого имеется разрешение на выполнение всех файлов внутри него. Правда, это правило не является обязательным — конечно же, можно разместить файлы сценария где душе угодно (не забыв проставить соответствующие права на каталог в настройках сервера). На мой взгляд, логично хранить файлы сценариев там, где это наиболее вам удобно, а не пользоваться общепринятыми штампами. Теперь наберем в адресной строке браузера:
http://www.myhost.com/cgi-bin/script.cgi
Мы получим нашу HTML-страницу. Заметьте, что при нажатии Reload
(а также при повторном посещении страницы) браузер перезагрузит страницу целиком, а не возьмет ее копию из своего кэша (это можно видеть по постоянно изменяющемуся случайному числу или по лампочкам модема). Мы добились такого результата благодаря заголовку


Pragma: no-cache
Давайте теперь посмотрим, что нужно изменить в нашем сценарии, чтобы его вывод представлял из себя с точки зрения браузера не HTML-документ, а рисунок. Пусть нам нужен сценарий, который бы передавал пользователю какой-то GIF-рисунок (например, выбираемый случайным образом из некоторого списка). Делается это абсолютно аналогично: выводим заголовок
Content-type: image/gif
Затем копируем один-в-один нужный нам GIF-файл в стандартный поток вывода (лучше всего — функцией fwrite, т. к. иначе могут возникнуть проблемы с "бинарностью" GIF-рисунка). Теперь можно использовать этот сценарий даже в таком контексте:
... êàêîé-òî òåêñò ñòðàíèöû ...
<img src=http://www.myhost.com/cgi-bin/script.cgi>
... ïðîäîëæåíèå ñòðàíèöû ...
В результате таких действий в нашу страницу будет подставляться каждый раз случайное изображение, генерируемое сценарием. Разумеется, чтобы избежать неприятностей с кэшированием, которое особенно интенсивно применяется браузерами по отношению к картинкам, мы должны его запретить выводом соответствующего заголовка. Именно так устроены графические счетчики, столь распространенные в Интернете.
Еще раз обращаю ваше внимание на такой момент: CGI-сценарии могут использоваться не только для вывода HTML-информации, но и для любого другого ее типа — начиная с графики и заканчивая звуковыми MIDI-файлами. Тип документа задается в единственном месте — заголовке Content-type. Не забывайте добавлять этот заголовок, в противном случае пользователю будет отображена стандартная страница сервера с сообщением о 500-й ошибке (для сервера Apache), из которой он вряд ли что поймет.

фотоальбом


Давайте напишем небольшой сценарий, представляющий собой простейший фотоальбом с возможностью добавления в него новых фотографий.

Листинг 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?

  if(!ereg("^(.*)\\.(gif|jpg|png)$",$e,$P)) continue;

  // Если нет, переходим к следующему файлу,

  // иначе обрабатываем этот

  $path="$ImgDir/$e";      // адрес

  $sz=GetImageSize($path); // размер

  $tm=filemtime($path);    // время добавления

  // Вставляем изображение в массив $Photos

  $Photos[$tm] = array(

    'time' => filemtime($path), // время добавления

    'name' => $e,               // имя файла

    'url'  => $path,            // его URI  

    'w'    => $sz[0],           // ширина картинки

    'h'    => $sz[1],           // ее высота

    'wh'   => $sz[3]            // "width=xxx height=yyy"

  );

}

// Ключи массива $Photos — время в секундах, когда была добавлена

// та или иная фотография. Сортируем массив: наиболее "свежие"

// фотографии располагаем ближе к его началу.

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="Закачать новую фотографию">


</form>
<?foreach($Photos as $n=>$Img) {?>
  <img
   src=<?=$Img['url']?>
   <?=$Img['wh']?>
   alt="Добавлена <?=date("d.m.Y H:i:s",$Img['time'])?>"
  >
<?}?>
</body>
Конечно, этот сценарий далеко не идеален (например, он не поддерживает удаление фотографий из фотоальбома), но для иллюстрации заявленных возможностей, по-моему, вполне подходит. Для простоты я совместил две функции (администрирование альбома и его просмотр) в одной программе. В реальной жизни, конечно, за каждую из них должен отвечать отдельный сценарий (первый из них, наверное, будет требовать от пользователя прохождения авторизации, чтобы добавлять фотографии в альбом могли лишь привилегированные пользователи).

Обратите внимание на то, как этот сценарий оформлен. В самом начале находится весь код на PHP, который, собственно, и работает с данными фотоальбома. В этом коде в принципе нет никаких указаний на то, как должна быть отформатирована страница. Его задача — просто сгенерировать данные. Наоборот, тот текст, который следует после закрывающей скобки ?>, содержит минимум кода на PHP. Его главная задача — оформить страницу так, чтобы она выглядела красиво. У меня нет никаких других стимулов, кроме как экономии типографской краски, чтобы не разнести данные блоки по разным файлам. Мы еще вернемся к такому подходу в одной из следующих глав.

Пример функции


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

Листинг 11.1. Пример функции

function GetMaxNum($arr, $max="")

{ // проходимся по всем элементам массива

  for($i=0,$n=-1; $i<count($arr); $i++) {

    // если этот элемент нам пока подходит, запоминаем его

    if((!Isset($m) || $arr[$i]>$m) && ($max==="" || $arr[$i]<$max)) {

      // сюда мы попадаем, когда очередной элемент больше текущего,

      // либо же текущего элемента еще не существует (первый проход)

      $m=$arr[$i]; // запоминаем текущий элемент

      $n=$i;       // запоминаем его номер

    }

  }

  return $n;

}

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

echo "Программа...";

function GetMaxNum($arr,$max)

{ ... тело функции ...

}

echo "Программа продолжается!";

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

Итак, мы создали функцию с именем GetMaxNum() и двумя параметрами, первый из которых рассматривается ей как массив, а второй — как вещественное число.

На самом деле на этапе создания функции еще никаких предположений о типах параметров не строится. Однако попробуйте нашей функции вместо массива в первом аргументе передать число — интерпретатор "заругается", как только выполнение дойдет до строчки с $arr[$i], и скажет, что "переменная не является массивом".[E50] 


Алгоритм работы функции таков: в цикле анализируем очередной элемент на предмет "максимальности": если он больше текущего максимального элемента, но меньше $max, он сам становится текущим максимумом, а его положение запоминается в $n. (Обратите внимание, что в описании функции параметр $max задается в виде $max="". Ýто означает, что если при вызове он будет опущен, то функция получит пустую строку в $max.)
После окончания цикла в $n окажется номер такого элемента (либо число -1, которое мы присвоили $n в начале). Его-то мы и возвращаем в качестве значения функции оператором return.
Ну вот, теперь в программе ниже описания функции можно написать:
$a=array(10,20,80,35,22,57);
$m=GetMaxNum($a,50);  // теперь $m=3, ò. å. $a[$m]=35

В действительности, поскольку фаза трансляции и исполнения в PHP разделены, мы можем применять вызовы функции еще до того, как она была описана. Однако это работает, конечно же, только в том случае, когда в момент интерпретации вызова функции ее код будет уже оттранслирован (например, вызов и описание функции происходят в одном и том же файле). Тем не менее, не советую вам злоупотреблять данной возможностью — лучше всегда поступать так, как это принято в Паскале: вызывать функции только после того, как они будут определены.
Зачем может понадобиться функция GetMaxNum() в реальной жизни? Например, для сортировки массива в порядке убывания с одновременным получением уникальных элементов. Конечно, это будет очень неоптимальный алгоритм, но для тренировочных целей он нам вполне подойдет (листинг 11.2):
Листинг 11.2. Сортировка с применением GetMaxNum()
function MySort($Arr)
{ $m= GetMaxNum($Arr)+1; // число, на 1 большее максимума в массиве
  while(($n=GetMaxNum($Arr,$m))!=-1)
    $New[]=$m=$Arr[$n];  // добавляем очередной максимальный элемент
  return $New;
}
// Пример вызова:
$Sorted=MySort(array(1,2,5,2,4,7,3,7,8));
// Теперь $Sorted===array(8,7,5,4,3,2,1)
Приведенная функция не изменяет исходный массив, а возвращает новый. В силу устройства функции GetMaxNum()
в результирующий массив будут помещены только уникальные элементы из $Arr, отсортированные в порядке убывания.

Функцию MySort()
можно ускорить примерно в 2 раза, если после каждой итерации удалять из массива $Arr обработанный элемент при помощи Unset(). Впрочем, это не так интересно, как может показаться.

Пример функции: Dump()


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

Функция выводит содержимое любой, сколь угодно сложной, переменной, будь то массив, объект или простая переменная. Как уже говорилось, приведенная функция исключительно полезна при отладке сценариев (которая в PHP пока еще не особенно развита).

В PHP версии 4 для аналогичных целей существуют две стандартных функции — print_r() и var_dump(), но листинг, который они выводят, довольно неудобен для восприятия человеком.

Листинг 11.16. Функция Dump()

// Вспомогательная функция, делающая всю "грязную" работу

function TextDump(&$Var,$Level=0)

{ if(is_array($Var)) $Type="Array[".count($Var)."]";

    else if(is_object($Var)) $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

    echo "<tt>",TextDump($Var),"</tt>\n";

}

В реальной жизни следует использовать функцию Dump(). Функция TextDump() (которая, по правде говоря, и делает всю работу) использует только одну неизвестную нам еще функцию — HtmlSpecialChars(), заменяющую в строке символы типа <, > или " на их HTML-эквиваленты (соответственно, &lt;, &gt; и &quot;). Мы применили дополнительную функцию для того, чтобы вывести сам результат, а главная функция занимается только форматированием этого результата (вставка его в тэги <pre> или <tt> в зависимости от размера вывода).



Пример использования функций поддержки MySQL


Ниже приводится пример комплексного использования описанных функций.

<?

// Соединяемся с сервером на локальной машине

mysql_connect("localhost");

// Выбираем текущую базу данных

mysql_select_db("my_database");

// Получаем все данные таблицы

$result = mysql_query("SELECT * FROM tbl");

// Запрашиваем идентификатор данных о полях таблицы

$fields = mysql_num_fields($result);

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

$rows   = mysql_num_rows($result);

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

$table = mysql_field_table($result,0);

echo "Таблица '$table' содержит $fields колонок и $rows строк<BR>";

echo "Таблица содержит следующие поля:<BR>";

// "Проходимся" по всем полям и выводим информацию о них

for($i=0; $i<$fields; $i++) {

  $type  = mysql_field_type($result, $i);

  $name  = mysql_field_name($result, $i);

  $len   = mysql_field_len($result, $i);

  $flags = mysql_field_flags($result, $i);

  echo "$type $name $len $flags<BR>\n";

}

?>



Пример использования оператора @


Вот еще один полезный пример использования оператора @. Пусть у нас имеется форма с submit-кнопкой, и нам нужно в сценарии определить, нажата ли она. Мы можем сделать это так:

<?

if(!empty($submit)) echo "Êíîïêà íàæàòà!";

. . .

?>

Но, согласитесь, следующий код куда элегантнее:

<?

if(@$submit) echo "Êíîïêà íàæàòà!"

?>

<form action=<?=$SCRIPT_NAME?> method=post>

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

</form>

Старайтесь чаще пользоваться оператором @ и реже — установкой слабого контроля ошибок.



печать дерева каталогов


В заключение приведу пример программы, которая рекурсивно распечатывает список всех каталогов (доступных сценарию) в вашей системе, начиная от корневого.

Листинг 16.1. Печать дерева каталогов в файловой системе

<?

// Функция распечатывает имена всех подкаталогов в текущем каталоге,

// выполняя рекурсивный обход. Параметр $level задает текущую

// глубину рекурсии.

function PrintTree($level=1)

{

  // Открываем каталог и выходим в случае ошибки

  $d=@opendir(".");

  if(!$d) return;

  while(($e=readdir($d))!==false) {

    // Игнорируем элементы .. и .

    if($e=='.'||$e=='..') continue;

    // Нам нужны только подкаталоги

    if(!@is_dir($e)) continue;

    // Печатаем пробелы, чтобы сместить вывод

    for($i=0; $i<$level; $i++) echo "  ";

    // Выводим текущий элемент

    echo "$e\n";

    // Входим в текущий подкаталог и печатаем его

    if(!chdir($e)) continue;

    PrintTree($level+1);

    // Возвращаемся назад

    chdir("..");

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

    // для больших распечаток

    flush();

  }

  closedir($d);

}

// Выводим остальной текст фиксированным шрифтом

echo "<pre>";

echo "/\n";

// Входим в корневой каталог и печатаем его

chdir("/");

PrintTree();

echo "</pre>";

?>

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

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



переопределение обработчиков


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

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

Код листинга 25.2 довольно велик, но не сложен. Тут уж ничего не поделаешь— нам в любом случае приходится задавать все 6 обработчиков, а это выливается в "объемистые"

описания.

Листинг 25.2. Переопределение обработчиков сессии

<?

// Âîçâðàùàåò ïîëíîå èìÿ ôàéëà âðåìåííîãî õðàíèëèùà ñåññèè.

//  ñëó÷àå, åñëè íóæíî èçìåíèòü òот каталог, â êîòîðîм äîëæíû

// õðàíèòüñÿ ñåññèè, äîñòàòî÷íî ïîìåíÿòü òîëüêî ýòó ôóíêöèþ

function ses_fname($key)

{

   return "sessiondata/".session_name()."/$key";

}

// Çàãëóøêè — ýòè ôóíêöèè ïðîñòî íè÷åãî íå äåëàþò

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($fname)),0777);
   @mkdir(dirname($fname),0777);
   // Ñîçäàåì ôàéë è çàïèñûâàåì â íåãî äàííûå ñåññèè


   $f=@fopen($fname,"wb");      if(!$f) return "";
   fwrite($f,$val);
   fclose($f);
   return true;
}
// Âûçûâàåòñÿ ïðè óíè÷òîæåíèè ñåññèè
function ses_destroy ($key)
{
   return @unlink(ses_fname($key));
}
// Ñáîðêà ìóñîðà — èùåì âñå ñòàðûå ôàéëû è óäàëÿåì èõ
function ses_gc($maxlifetime)
{
   $dir=ses_fname(".");
   // Ïîëó÷àåì äîñòóï ê каталогу òåêóùåé ãðóïïû ñåññèè
   $d=@opendir($dir); if(!$d) return false;
   $DelDir=1; // Ïðèçíàê òîãî, ÷òî каталог ïóñò, è его ìîæíî óäàëèòü
   // ×èòàåì âñå ýëåìåíòû êàòàëîãà
   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",
   "ses_read", "ses_write",
   "ses_destroy", "ses_gc"
);
// Äëÿ ïðèìåðà ïîäêëþ÷àåìñÿ ê ãðóïïå ñåññèé test
session_name("test");
session_start();
session_register("count");
// Äàëüøå êàê îáû÷íî...
$count=@$count+1;
?>
<body>
<h2>Ñ÷åò÷èê</h2>
 òåêóùåé ñåññèè ðàáîòû ñ áðàóçåðîì Âû îòêðûëè ýòó ñòðàíèöó
<?=$count?> ðàç(à). Çàêðîéòå áðàóçåð, ÷òîáû îáíóëèòü ýòîò ñ÷åò÷èê.
</body>

Пример первый


Наверняка вам приходилось когда-нибудь сталкиваться с такой ситуацией

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

на это место out.

Проблема довольно тривиальна, и даже на PHP ее можно решить всего несколькими командами. Например, так:

$p=strrpos($inFile,'.');

if($p) $outFile=substr($inFile,0,$p); else $outFile=$inFile;

$outFile.=".out";

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

имеет место, [E86]несмотря на то, что само действие приведенных строк можно описать всего несколькими словами. А именно: "Замени в строке $inFile все, что после последней точки (и ее саму), или, в крайнем случае, "конец строки"

на строку .out, и присвой это переменной $outFile".



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


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

Листинг 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-01 23:59:59 GMT", Buf);
    // Òåïåðü ýòî — íîâîå çíà÷åíèå Cookie
    Cook=Buf;
  }
// âûâîäèì ñòðàíèöó ñ ôîðìîé
  printf("Content-type: text/html\n\n");
  printf("<html><body>\n");
// åñëè èìÿ çàäàíî (íå ïóñòàÿ ñòðîêà), ïðèâåòñòâèå
  if(strcmp(Cook, ""))
    printf("<h1>Ïðèâåò, %s!</h1>\n",Cook);
// ïðîäîëæàåì
  printf("<form action=/cgi-bin/script.cgi method=get>\n");
  printf("Âàøå èìÿ: ");
  printf("<input type=text name=name value=’%s’>\n",Cook);
  printf("<input type=submit value=’Îòïðàâèòü’>\n");
  printf("</form>\n");
  printf("</body></html>");
}
Теперь при первом заходе на этот URL пользователь получит форму с пустым полем для ввода имени. Если он что-то туда напечатает и нажмет кнопку отправки, его информация запомнится браузером. Итак, посетив в любое время до 31 декабря 2001 года этот же URL, он увидит то, что напечатал давным-давно в текстовом поле. И, что самое важное, — его информацию "увидит" также и сценарий. Кстати, у злоумышленника нет никаких шансов получить значение Cookie посетителя, потому что оно хранится у него на компьютере, а не на сервере.
И опять я намекаю на то, что использование Си и на этот раз довольно затруднительно. Неудобно URL-декодировать и кодировать при установке Cookies, накладно разбирать их на части, да и вообще наша простая программа получилась слишком длинной. Не правда ли, приятно будет обнаружить, что в PHP все это реализовано автоматически: для работы с Cookies существует всего одна универсальная функция SetCookie(), а получение Cookies от браузера вообще не вызовет никаких проблем, потому что оно ничем не отличается от получения данных формы. Это логично. В самом деле, какая нам разница, какие данные пришли из формы, а какие — из Cookies? С точки зрения сценария — все равно...
Но не буду забегать вперед. Займемся пока теорией авторизации.

Пример счетчика


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

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

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

Листинг 15.4. Простейший текстовый счетчик

<?

$f=fopen("counter.dat","a+");

flock($f,LOCK_EX);    // Говорим, что дальше будем работать только мы

$count=fread($f,100); // Читаем значение, сохраненное в файле

@$count=$count+1;     // Увеличиваем его на 1 (пустая строка = 0)

ftruncate($f,0);      // Стираем файл

fwrite($f,$count);    // Записываем новое значение

fflush($f);           // Сбрасываем файловый буфер

flock($f,LOCK_UN);    // Отключаемся от блокировки

fclose($f);           // Закрываем файл

echo $count;          // Печатаем величину счетчика

?>

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



Пример второй


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

$slash1=strrpos($fullPath,'/');

$slash2=strrpos($fullPath,'\\');

$slash=max($slash1,$slash2);

$dirName=substr($fullPath,0,$slash);

$fileName=substr($fullPath,$slash+1,10000);

Здесь мы воспользовались тем фактом, что strrpos() возвращает false, которое интерпретируется как 0, если искомый символ не найден. Обратите внимание на то, что пришлось два раза вызывать strrpos(), потому что мы не знаем, какой слэш будет получен от пользователя — прямой или обратный. Видите — код все увеличивается. И уменьшить его почти невозможно.

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

Опять же, сформулируем словами то, что нам нужно: "Часть слова после последнего прямого или обратного слэша или, в крайнем случае, после начала строки, присвой переменной $fileName, а "начало строки" — переменной $dirName". Формулировку "часть слова после последнего слэша" можно заменить на несколько другую: "Часть слова, перед которой стоит слэш, но в нем самом слэша нет".



Примеры использования регулярных выражений


Какая же книга, описывающая (даже вкратце) регулярные выражения, обходится без примеров…. Я не буду отступать от установленных канонов, хотя, конечно, понимаю, что истинная свобода при работе с выражениями достигается только практикой. Некоторые из следующих ниже примеров выглядят довольно сложно, но, если разобраться, смысл чаще всего оказывается на поверхности.



Принудительное завершение программы


void exit()

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

void die(string $message)

Функция делает почти то же самое, что и exit(), только перед завершением работы выводит строку, заданную в параметре $message. Чаще всего ее применяют, если нужно напечатать сообщение об ошибке и аварийно завершить программу.

Полезным примером применения die() может служить такой код:

$filename='/path/to/data-file';

$file=fopen($filename, 'r') or die("íå ìîãó îòêðûòü ôàéë $filename!");

Здесь мы ориентируемся на специфику оператора or— "выполнять"

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

$filename='/path/to/data-file';

($file=fopen($filename, 'r')) || die("íå ìîãó îòêðûòü ôàéë $filename!");

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



Присвоение значения


Мы можем присвоить переменной значение другой переменной (или значение, возвращенное функцией), ссылку на другую переменную, либо же константное выражение (за исключением объектов, для которых вместо этого используется оператор new). Как уже говорилось, за преобразование типов отвечает сам интерпретатор. Кроме того, при присваивании старое содержимое и, что самое важное, тип переменной теряются, и она становится абсолютно точной копией своего "родителя". То есть, если мы массиву присвоим число, это сработает, однако весь массив при этом будет утерян.



Прямой перебор массива


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

цикла одновременно получать и ключ, и значение текущего элемента.



Прямоугольники


int imageFilledRectangle(int $im,int $x1,int $y1,int $x2,int $y2,int $c)

Название этой функции говорит за себя: она рисует закрашенный прямоугольник в изображении, заданном идентификатором $im, цветом $col (полученным, например, при помощи функции imageColorAllocate()). Координаты ($x1,$y1) и ($x2,$y2) задают координаты верхнего левого и правого нижнего углов, соответственно (отсчет, как обычно, начинается с левого верхнего угла и идет слева направо и сверху вниз).

Эта функция часто применяется для того, чтобы целиком закрасить только что созданный рисунок, например, прозрачным цветом:

$i=imageCreate(100,100);

$c=imageColorAllocate($i,0,0,0);

imageColorTransparent($i,$c);

imageFilledRectangle($i,0,0,imageSX($i)-1,imageSY($i)-1,$c);

// äàëüøå ðàáîòàåì ñ èçíà÷àëüíî ïðîçðà÷íûì ôîíîì

int imageRectangle(int $im, int $x1, int $y1, int $x2, int $y2, int $col)

Функция imageRectangle() рисует в изображении прямоугольник с границей толщиной 1 пиксел цветом $col. Параметры задаются так же, как и в функции imageFilledRectangle().