Веб-программирование на PHP. (Часть II)

Руслан Курепин
http://kurepin.ru/main.phtml

Веб-программирование на PHP (Часть I)

Краткость -- сестра таланта
Работа с файлами (чтение-запись)
Добавляем текст в базу (начало)
Добавляем текст в базу (продолжение)
Флаг enable и удаление файлов
class_out (начало)
Письма с вложенными файлами (начало)
Письма с вложенными файлами (продолжение)
Письма с вложенными файлами (финал)

Веб-программирование на PHP (Часть III)


^^^   Краткость - сестра таланта

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

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

Знакомьтесь -- точка.

Точку "." вы уже знаете, она объединяет строки. Плюс "+" -- складывает цифры, а точка объединяет строки.

$a1="100";
$a2="200"

Что будет, если сложить $a1 и $a2? А это как сложить...

$a1+$a2=300
$a1.$a2="100200"

...и никогда об этом не забывайте.

А вот как еще можно записать сложение.

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

$this->string_about_letters = $this->string_about_letters."несколько букв...";
$this->string_about_letters = $this->string_about_letters." еще несколько букв...";
$this->string_about_letters = $this->string_about_letters." и еще несколько букв...";
$this->string_about_letters = $this->string_about_letters." снова несколько букв...";

Несколько длинновато, не так ли? Мешает повторение длинной переменной $this->string_about_letters. Поэтому, запись сделаем иначе:

$this->string_about_letters .= "несколько букв...";
$this->string_about_letters .= " еще несколько букв...";
$this->string_about_letters .= " и еще несколько букв...";
$this->string_about_letters .= " снова несколько букв...";

Удобнее, не так ли?

Тоже самое, касается и математического сложения:

$abc+=1;

Прибавить 1 к содержимому переменной $abc. А как еще можно прибавить единицу? Обычно, по С-шному:

$abc++; или ++$abc;

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

$a=1;
echo $a++;
echo ++$a;

Первое echo распечатает нам "1", а второе распечатает?.. а вот не правильно! Не 2 оно распечатает, а "3". Почему? Догадайтесь сами.

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

Вы уже знаете, как сделать запрос в базу, но мы толком не говорили о том, как разбирать полученные данные. А между тем, это можно делать различными способами. Давайте сразу на примере. Предположим, мы выбираем из базы видеокассет наименования видеофильмов, выпущенных в 2000 году, и хотим распечатать их в виде таблицы (с ID кассеты, конечно). Вот, можно было бы оформить процедуру так (во многих учебниках вы и найдете нечто похожее):

$this->sql_query="select * from film where f_date between '2000-01-01 00:00:00'
 and '2000-12-31 23:59:59 order by f_id'";
$this->sql_execute();

$str="<table>";
$row_count=mysql_num_rows($this->sql_res);
for($i=0;$i<$row_count;$i++)
{
 $film=mysql_fetch_array($this->sql_res);
 $str=$str."<tr><td>".$film['f_id']."</td><td>".$film['f_name']."</td></tr>\n";
}
$str=$str."</table>";

Поясняю.

Мы делаем запрос в базу (кстати, обратите внимание на новое для нас условие в запросе: between date and date, такая форма часто используется для указания диапазона дат).

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

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

Затем мы открываем цикл с количеством повторений, равным количеству полученных строк из запроса.

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

Ассоциативный массив -- это тоже самое, что и обычный (нумерованный) массив, только в качестве имен для ячеек используются не цифры, а цепочки символов. И обращаться к ячейкам такого массива следует соответствующе: $abc['first'], $abc['mama']...

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

Обратите особое внимание: обращаться к ячейкам ассоциативных массивов в текстовых строках нельзя! Надо обязательно разрывать строку и "вклеивать" значения при помощи точек(.), как показано.

Цикл повторяется нужное количество раз, после чего мы замыкаем $str последним html-тэгом. Готово. Но не слишком ли это все громоздко? По-моему, очень даже. Предлагаю записать это все иначе: все тоже самое, но короче.

$this->sql_query="select f_id, f_name from film where f_date between
 '2000-01-01 00:00:00' and '2000-12-31 23:59:59 order by f_id'";
$this->sql_execute();

$str="<table>";
while(list($f_id,$f_name)=mysql_fetch_row($this->sql_res))
{
 $str=$str."<tr><td>$f_id</td><td>$f_name</td></tr>\n";
}
$str=$str."</table>";

Вот так. Что изменилось?

Во-первых, мы в запросе сразу конкретизировали: какие поля из базы нам нужны, и в каком порядке нам их подать: f_id, f_name.

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

В-третьих, мы в самом условии цикла while получаем в отдельные переменные нужные нам значения: list() изображает из себя массив, где ячейками массива являются переменные, которые мы укажем. То есть мы сразу получаем в переменную $f_id номер видеофильма, а в f_name получаем его название.

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

Посмотрите еще раз на второй вариант записи этой функции. Краткость -- сестра таланта, не забывайте об этом даже тогда, когда пишите большой web-проект. Лучше десять раз подумать, как написать короче, чем плодить километровые листинги. Заказчик, правда, не всегда оказывается доволен маленьким объемом исходных текстов, не понимая, за что же он платит деньги. Но тут уже вы должны ему объяснить, что платит он не за машинопись, а за талант! Чем короче программа, тем она быстрее выполняется и тем меньше занимает оперативную память, что тоже бывает важно...


^^^   Работа с файлами (чтение-запись)

Если вы начинающий программист, то наверняка привыкли работать так: строчку добавил — проверил выполнение, еще строчку добавил — опять проверил, как программа работает. А мы тут пишем, но не проверяем. Все верно, не проверяем. Более того, я вам обещаю, что в тех листингах, что мы уже написали, достаточно много ошибок. В основном, конечно, синтаксических. Где-то забыли точку с запятой поставить, где-то $this-> к переменной забыли приписать. Да мало ли что еще... Но это так и надо. Я не кривлю душой, я действительно все пишу прямо в текстах курса, без какой-либо проверки их в работе.

Так вот, друзья мои. Привыкайте писать именно так! Рождайте в голове задумки и выкладывайте их в виде программного кода. Проверить и отладить вы их всегда успеете. А как показывает практика, в процессе написания нового модуля очень даже часто возникает "задним числом" какая-то новая идея, заставляющая нас возвращаться назад и что-то переделывать. И что, снова тестировать и выискивать глюки? Да ничего подобного - так можно всю жизнь писать один проект. Но я вам обещаю. Еще несколько занятий, и мы перейдем к классу class_out, перейдем к визуализации данных и добавления их через web-форму, с использованием написанных функций. Готовьте дизайн для вашего нового сайта!

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

В PHP существует достаточно много разных механизмов для работы с файлами различных типов. Мы разделим все файлы на два основных типа: текстовые файлы и файлы данных (картинки, музыка, исполняемый код и все остальные типы файлов). Отличие между текстовыми файлами и остальными состоит в том, что текстовые файлы можно разделить на строки (окончанием строки служит символ EOL — "\n" — Enter). И по этим строкам файл можно читать, писать и все остальное. Для начала вспомним, что во всех системах файл необходимо открыть, прежде чем что-то с ним сделать.

Открыть файл нам поможет функция PHP fopen(). Оформляется открытие файла так:
$r=fopen('path_to_file','mode');
где:
$r — указатель на открытый файл. Он нам нужен, чтобы обращаться к нужному файлу, когда их открыто более одного.
path_to_file — абсолютный путь к файлу на диске сервера.
mode — режим, в котором открывается файл.

В PHP можно открыть файл в следующих режимах:

'r' — только для чтения.

'r+' — для чтения и записи

'w' — только для записи

'a-' — только для записи. То есть файл открывается для записи, но при этом курсор устанавливается в конец файла. Можно сказать, что это открытие файла для дозаписи.

'a+' — тоже, что и a-, но еще доступно и чтение.

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

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

'r' — читать (read)

'w' — писать (write)

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

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

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

Прежде чем перейти к чтению и записи содержимого файла, давайте его закроем:

fclose($r);

Не сложно. $r — это указатель на открытый файл (вспомните функцию открытия файла).

После выполнения скрипта PHP сам закроет все файлы, которые вы забыли закрыть. Но если вам важно сохранить в файлах именно то, что надо — сделайте это сами.

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

$file_name='/home/roma/address.txt'; // 1
$r=fopen($filename,'r'); // 2
$text=fread($r,filesize($file_name)); // 3
fclose($r);  // 4
$text=ereg_replace('213-','670-',$text); // 5
$w=fopen($file_name,'w'); // 6
fwrite($w,$text);  // 7
fclose($w);  // 8

По строчкам:

1. Определили в переменную путь к файлу. Представим, что в этом файле содержится копия вашей записной книжки.

2. Открываем этот файл для чтения.

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

4. Закрываем файл.

5. А почему мы все это делаем? А потому, что у массы наших друзей сменились первые три цифры телефона: наконец сменили старую АТС на новую цифровую. Функция PHP preg_replace поможет нам заменить все 213- на 670- по всему содержимому переменной $text. А измененный вариант мы записываем обратно в $text.

6. Открываем все тот же файл, но теперь для записи.

7. Записываем в файл содержимое переменной $text.

8. Закрываем файл.

Вот так. Тоже все не сложно.

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

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

$file_name='/home/roma/address.txt'; // 1
$file_new_name='/home/roma/address_new.txt'; // 2
$r=fopen($filename,'r'); // 3
$w=fopen($file_new_name,'w'); // 4
while($str=fgets($r,1024)) // 5
{
  $str=ereg_replace('213-','670-',$str); // 6
  fputs($w,$str); // 7
}
fclose($r);  // 8
fclose($w);  // 9

1. Определяем путь к файлу для чтения

2. Определяем путь к файлу для записи

3. Открываем файл для чтения

4. Открываем другой файл для записи

5. Начинам читать по одной строке в переменную $str из файла $r до тех пор, пока не достигнем конца файла (EOF — End Of File). Причем, строка читается либо до знака конца строки (EOL — End Of Line), либо до 1024-го символа. Это свойство функции чтения строки fgets().

6. Проводим замену 213 на 670.

7. Записываем строку $str в файл $w.

8, 9. После окончания цикла закрываем оба файла.

Немного длиннее получилось, но насколько правильнее!

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

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


^^^   Добавляем текст в базу (начало)

Пишем процедуру добавления в базу нашего сайта нового текста. По крайней мере — начнем писать. На какие этапы логично разбить эту задачу? Мне думается, что на такие:

1. Проверка данных на корректность.
2. Приведение текста к нужному виду
3. Сохранение текста на диске
4. Добавление необходимых данных в БД.

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

var $this->in_text; // сам текст
var $this->in_text_name; // название текста
var $this->in_text_id; // id текста
var $this->in_text_dt; // дата добавления
var $this->in_text_cat; // рубрика

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

alter table tbl_texts add column t_cat int not null;

Все. Теперь есть! А те, кто читают данный курс не подряд — пожалеют об этом, и не один раз (тут злобная ухмылка). Для тех, кто читает подряд, перевожу: изменить таблицу tbl_texts, добавив в нее целочисленное поле t_cat. Свою ошибку удачно исправили, теперь будем проверять на ошибки данные пользователя, управляющего нашей системой (готовьте class_utils — добавлять описания новых ошибок). Нам много не потребуется:

function in_text_data_check()
 {
  $this->in_text_name=AddSlashes($this->in_text_name);
  if(strlen($this->in_text_name)==0) return(24);
  if(strlen($this->in_text_name)>200) return(25);
  
  return(0);
 }

Тут и пояснять нечего. Добавьте в class_ulils две новые ошибки:

$err[24]="Вы не задали название тексту";
$err[25]="Длинна названия текста превышает допустимые 200 символов";

и поехали дальше...

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

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

На самом деле, храните тексты просто в двух видах. Тогда ничего выгрызать или заменять не придется. При сегодняшних объемах дискового пространства это вполне доступно. В данном случае я просто хочу оставить за собой повод вернуться к "парсингу", когда дойдем до почтовых рассылок или чего-то подобного.

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

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

Предлагаю такие условия для добавляемого текста:

1. Текст не должен превышать 100Кб.
2. Текст не должен быть короче 100 символов.
3. Подгружаемый текст не должен иметь html-тэгов и спецсимволов, кроме: <a>, <b>, <i>, <u>, <img>... м-м-м-м... <div>. Хватит пока. Все остальные символы мы просто выкинем в помою. Попробуем реализовать задуманное....

function in_text_adapt()
 {
  $this->in_text=strip_tags($this->in_text,"<a><b><u><img><div>");
  $this->in_text=nl2br($this->in_text);
  if(strlen($this-in_text)<201) return(26);
  if(strlen($this->in_text)>102400) return(27);
  return(0);
 }

В общем-то, это все. Только не забывайте описывать ошибки:

$err[26]="Текст слишком короткий";
$err[27]="Текст слишко длинный";

На что следует обратить внимание в этой функции.

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

Второй строкой мы перед каждым переносом строки (EOL — "\n" — Enter) добавили тэг <br>, указывающий браузеру на перенос строки.

Вынужден признаться: никогда в жизни не пользовался функцией nl2br(). И вам не советую. Она вам вместо <br> наставит XHTML-совместимых <br />. Оно вам надо? Замените эту функцию простейшим регекспом:
 
$this->in_text=preg_replace("/\n/","<br>\n",$this->in_text);
 
и всего делов.

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


^^^   Добавляем текст в базу (продолжение)

Если я ничего не путаю, то мы приготовили данные для того, чтобы добавить их в базу нашего сайта. Just do it!..

function in_text_add()
 {
  // Проверяем данные для БД
  $err=$this->in_text_data_check();
  if($err) return($err);

  // Адаптируем текст для сохранения
  $err=$this->in_text_adapt();
  if($err) return($err);

  // Сначала заносим в базу данные
  $this->sql_query="insert into tbl_texts(t_name, t_dt, t_cat)
 values('".$this->in_text_name."', now(), '".$this->in_text_cat."')";
  $this->sql_execute();
  if($this->sql_err) return(11);

  // Получаем сгенерированный базой ID добавленного текста
  $this->sql_query="select last_insert_id";
  $this->sql_execute();
  if($this->sql_err) return(11);

  list($this->in_text_id)=mysql_fetch_row($this->sql_res);

  // Теперь пишем текст в директорию data, в  файл с номером ID
  if($w=fopen($this->PATH_DATA."/".$this->in_text_id,'w'))
  {
   fwrite($w,$this->in_text);
   fclose($w);
  } else
  {
   $this->sql_query="delete from tbl_texts where t_id='".$this->in_text_id."'";
   $this->sql_execute();
   return(31);
  }

  return(0);
 }

Зачитываю собственные комментарии.

Вызываем функцию проверки данных для БД и функцию адаптации текста. Если одна из них вернет ошибку, то мы останавливаемся с возвратом этой самой ошибки.

Заносим в БД новую запись: название текста, дату, номер рубрики.

Помните, как в таблице было декларировано поле t_id? Оно было декларировано как auto_increment. А это значит, что при добавлении новой записи содержимое этого поля определяется автоматически -- плюс один к последнему добавленному в базу значению этого поля.

Вот нам это самое число и надо выдернуть из базы. Это же уникальный идентификатор добавленного текста! Он вполне может служить нам и уникальным именем файла для хранения текста на диске.

В этом нам поможет SQL-запрос select last_insert_id. Очень короткий запрос, который возвратит нам последний сгенерированный номер.

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

Записали идентификационный номер в переменную $this->in_text_id.

Теперь можно записать и файл на диск.

Вот тут, мы делаем проверку на открытие файла.

if(fopen(:.

А имя файла мы формируем из переменной $this->PATH_DATA (которую описали в классе class_vars) и идентификационного номера текста, который получили из базы (in_text_id).

Если открытие файла для записи прошло удачно, то мы записываем в файл содержимое переменной $this->in_text и закрываем файл.

Если же открытие прошло с ошибкой, то положительное тело if выполняться не будет, а выполнится тело false, в котором мы возвращаем все на круги своя: удаляем из таблицы tbl_texts запись, которую только что сделали: "удалить из таблицы tbl_texts запись, где идентификационный номе равен $this->in_text_id". И затем уже возвращаем код ошибки 31.

Рекомендую этот номер ошибки записать в class_utils как:

$err[31]="Не могу открыть файл для записи: (".$this->PATH_DATA."/".$this->in_text_id.")";

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

Вот так. Мы уж умеем добавлять, изменять и удалять данные в таблице рубрик. Умеем добавлять новый текст в базу сайта. Что нам еще надо? Я так думаю, что надо уметь удалять из базы ошибочный или устаревший текст. И снабдить текст атрибутом "hidden", чтобы прятать его от посторонних глаз, когда он не нужен на сайте, или просто еще не готов к публикации.

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

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

Второе задание (для гурманов). Как должна выглядеть функция in_text_adapt() из прошлого урока, чтобы текст, добавленный через браузер в кодировке KOI-8, отлавливался, и преобразовывался в win-1251 (в win-1251 мы записываем текст на диск).

Свои варианты решений присылайте на форум, или мне лично (я их тогда сам выложу в форум). Там их и обсудим.


^^^   Флаг enable и удаление файлов

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

Постановщик — ставит задачу. Ставит ее на родном языке, да так, чтобы она всем была понятна. Особенно программисту.

Программист — ломает голову над алгоритмами решения тех задач, которые поставил постановщик задач. А потом вдалбливает эти алгоритмы в голову кодировщику.

Кодировщик — пишет всякие тексты, понятные только компьютеру. Это последнее звено в приготовлении программного продукта.

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

Прежде всего, давайте вот какой вопрос решим. Как сделать так, чтобы мы могли снимать и ставить материалы на сайт быстро и с легкостью?

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

Раз у нас есть база данных, значит надо добавить в таблицу текстов флажок, указывающий на "видимость" материала. Этот флажок можно назвать, например, t_enable. Или t_on. Мне больше нравится t_enable.

Модифицировать таблицу мы уже умеем, поэтому разжевывать следующий запрос в SQL я не буду:

alter table tbltexts add column t_enable enum("Y","N") not null default "N";

Кто такой enum() и с чем его едят — вы можете прочесть тут: 6.2.3.3 The ENUM Type.

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

Теперь, о чем вы уже наверняка догадались, надо написать функцию, которая будет включать или выключать текст на сайте. Назовем эту функцию in_text_enable(). Добавьте в class_in переменную in_text_enable:

$this->in_text_enable;

и приступим к написанию коротенькой функции.

function in_text_enable()
 {
  if($this->in_text_enable!='Y') $this->in_text_enable='N';
  $this->in_text_id=(int)$this->in_text_id;
  
  $this->sql_query="update tbl_texts set t_enable='".$this->in_text_enable."
' where t_id='".$this->in_text_id."'";
  $this-sql_execute();
  if($this->sql_err) return(11);

  return(0);
 }

Как видите, ничего особенно умного тут придумывать не надо. Достаточно присвоить переменной in_text_id номер текста, переменной in_text_enable статус (Yes/No) и вызвать нашу свежеиспеченную функцию in_text_enable().

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

Посудите сами. Переменная in_text_enable может нести в себе только значение "N" или "Y": все другие значения напрочь отметаются первой строкой тела функции. Либо переменная равна 'Y', либо мы ей тут же присваиваем значение "N".

Честно говоря, эту проверку можно было бы тоже не производить — MySQL сам не пропустит значений, не входящих в перечислимый тип enum(). Но! Нам тогда надо было бы "прослешить" эту переменную, чтобы не допустить подстановки в нее взламывающего кода, а потом еще обрабатывать сообщение об ошибке от mysql, если значение окажется не 'Y' и не 'N'. Поэтому, проще привести данные к их точному виду и ни о чем не волноваться.

Переменная же in_text_id после второй строки тела функции не может быть ничем иным, кроме как целым числом. Это так же защитит нас от взлома.

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

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

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

function in_text_remove()
 {
  $this->in_text_id=(int)in_text_id;

  $this->sql_query="delete from tbl_texts where t_id='".$this->in_text_id;
  $this->sql_execute();
  if($this-sql_err) return(11);

  if(!unlink($this->PATH_DATA."/".$this->in_text_id)) return(32);

  return(0);
 }

Тоже простая функция, комментировать построчно я ее не стану. Лучше обратите внимание на порядок выполнения: сначала мы удаляем текст из БД, а только потом с диска, а не наоборот! Почему? А представьте себе, что будет, если мы сначала удалим файл с диска, а потом по какой-нибудь случайной причине не получится удалить файл из БД? В этом случае документ будет по-прежнему отображаться на сайте, а при его запросе пользователем будет выскакивать ошибка открытия файла на диске. Фу, как это не красиво!

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

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

Именно поэтому, мы должны были на нашем прошлом занятии сначала записать файл на диск, а потом уже добавлять его в базу данных. Но мы сделали наоборот, и я обязан объяснить почему. Мы не можем записать файл на диск, не узнав его ID. А узнать его ID мы можем только после добавления записи в базу. Казалось бы, что это заколдованный круг, но не тут-то было! У нас же теперь есть флаг t_enable, который защитит нас от ошибки. Даже если произошла ошибка записи на диск, то на сайте никаких изменений не произойдет, так как по умолчанию флаг t_enable установлен в 'N' и добавленный текст на сайте не показывается, до его подтверждения при помощи функции in_text_enable(). Вот такая военная хитрость!

И не забудьте добавить в файл class_utils описание новой ошибки:

$err[32]="Не могу удалить файл: ".$this->PATH_DATA."/".$this->in_text_id;


^^^   class_out (начало)

Предлагаю сегодня создать новый файл, в который мы поместим новый класс. Класс, отвечающий за визуализацию содержимого базы нашего сайта. Называться этот класс будет class_out, а храниться он будет в файле out.class. Мы его породим от класса class_utils, как и класс class_in.

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

Думаю, что оформить новый класс для вас не составит никакого труда.

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

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

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

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

В чем же, собственно, проблема. А в том, что классы можно рождать друг от друга хоть каждый раз, когда надо написать новую функцию. Удобно ли это? Или наоборот: разместить все перечисленные возможности сайта в один файл и тупо обращаться к нему с каждой html-страницы? Любая крайность -- плохо. Тут надо искать золотую середину. И будем мы ее искать вместе. Что предлагаю я. У меня уже выработалась своя техника составления древа файлов и классов. Она сильно похожа на ту, что я вам преподношу. Мы разделили функции общего назначения на три класса: vars, mysql, utils. Для удобства. Конечно, можно было их собрать в один файл и даже в один класс. Но это вы сможете сделать сами, если пожелаете. Тут никаких весомых доводов "за" или "против" привести, как мне кажется, не получится. "У каждого свой вкус: один любит арбуз, а другой -- свиной хрящик..."(с).

Далее мы написали класс добавления данных. Он у нас получился небольшой, но если бы мы на самом деле писали огромный проект, то функций добавления данных и функций управления данными было бы заметно больше. Стоило ли бы, в этом случае, разнести их на подклассы? Не думаю. Я бы не стал. Почему? Объясню. Когда мы пишем систему добавления и управления данными (back office это обычно называется), то никогда до конца не можем знать, в каком виде будут поступать данные и в каких комбинациях. И если мы начнем дробить систему добавления данных на разные классы, то рано или поздно начнем наталкиваться на ситуации, когда функции из одной ветки классов будут требоваться в классах других веток. К чему это все приведет? Правильно, приведет все это к большому бардаку!

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

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

Как это будет выглядеть. Берем за точку отсчета class_out, в который добавим функции, требуемые для любой страницы сайта. Затем, породим от него class_out_title, в который занесем функции, вызываемые только на титульной странице сайта. От того же class_out породим еще ветвь, например, -- class_out_news, которая будет нести в себе функции, связанные с новостной страницей. И так далее. Для каждого типа страницы мы создадим свой подкласс, рожденный от общего class_out, который и будем вызывать на этих самый страницах. Я не сильно запутал вас?

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

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


^^^   Письма с вложенными файлами (начало)

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

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

Как оказалось, мало кто умеет при помощи простой команды PHP mail() отправлять письма с вложенными файлами. Это очень популярный на сегодня вопрос по PHP, как оказалось. А между тем, все не так сложно, как кажется.

Давайте разберем этот интересный вопрос.

Что такое файл с вложениями?

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

Я оправляю сам себе письмо с вложенным файлом MS Word, содержащим данный текст на данной стадии его написания. Закрываю файл...

Открываю обратно... Вот, я уже получил свое письмо.

В моем любимом TheBat посмотреть исходник письма можно по кнопке F9. И что же мы видим? Уж простите, для полноты ощущений я привожу почти весь исходный текст письма.

Return-Path: 
Received: from 
  by atlanta.ru (CommuniGate Pro RULES 3.4.5)
  with RULES id 393466; Thu, 20 Dec 2001 14:20:36 +0300
X-Autogenerated: Mirror
X-Mirrored-by: 
Received: from 195.58.33.146 ([195.58.33.146] verified)
  by atlanta.ru (CommuniGate Pro SMTP 3.4.5)
  with ESMTP id 393465 for atos@21.ru; Thu, 20 Dec 2001 14:20:28 +0300
Date: Thu, 20 Dec 2001 14:17:31 +0300
From: Ruslan Kurepin 
X-Mailer: The Bat! (v1.53bis) Personal
Reply-To: Ruslan Kurepin 
X-Priority: 3 (Normal)
Message-ID: <36657745688.20011220141731@21.ru>
To: atos@21.ru
Subject: text
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="----------11552613C2F043A"

------------11552613C2F043A
Content-Type: text/plain; charset=koi8-r
Content-Transfer-Encoding: 8bit

рТЙЧЕФУФЧХА!

  ч ЬФП РЙУШНП ЧМПЦЕО ЖБКМ "РЙУШНБ У ЧМПЦЕОЙСНЙ.doc" чЕУПН Ч 20 480
  bytes.

-- 
 у хЧБЦЕОЙЕН,
 тХУМБО лХТЕРЙО                          mailto:atos@21.ru
 http://caricatura.ru
 http://www.21.ru
------------11552613C2F043A
Content-Type: application/msword; name==?koi8-r?B?0MnT2M3BINMg18zP1sXOydHNyS5kb2M=?=
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename==?koi8-r?B?0MnT2M3BINMg18zP1sXOydHNyS5kb2M=?=

0M8R4KGxGuEAAAAAAAAAAAAAAAAAAAAAPgADAP7/CQAGAAAAAAAAAAAAAAABAAAAIwAAAAAAAAAA
EAAAJQAAAAEAAAD+////AAAAACIAAAD/////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////s
pcEAOSAZBAAA8BK/AAAAAAAAEAAAAAAABAAAGgsAAA4AYmpiav3P/c8AAAAAAAAAAAAAAAAAAAAA
AAAZBBYAIhIAAJ+lAACfpQAAjQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//w8AAAAA
AAAAAAD//w8AAAAAAAAAAAD//w8AAAAAAAAAAAAAAAAAAAAAAGwAAAAAAKgAAAAAAAAAqAAAAKgA
AAAAAAAAqAAAAAAAAACoAAAAAAAAAKgAAAAAAAAAqAAAABQAAAAAAAAAAAAAALwAAAAAAAAA3gEA
AAAAAADeAQAAAAAAAN4BAAAAAAAA3gEAAAwAAADqAQAADAAAALwAAAAAAAAATQMAALYAAAACAgAA
AAAAAAICAAAAAAAAAgIAAAAAAAACAgAAAAAAAAICAAAAAAAAAgIAAAAAAAACAgAAAAAAAAICAAAA
AAAAzAIAAAIAAADOAgAAAAAAAM4CAAAAAAAAzgIAAAAAAADOAgAAAAAAAM4CAAAAAAAAzgIAACQA
AAADBAAAIAIAACMGAADCAAAA8gIAABUAAAAAAAAAAAAAAAAAAAAAAAAAqAAAAAAAAAACAgAAAAAA
AAAAAAAAAAAAAAAAAAAAAAACAgAAAAAAAAICAAAAAAAAAgIAAAAAAAACAgAAAAAAAPICAAAAAAAA
RAIAAAAAAACoAAAAAAAAAKgAAAAAAAAAAgIAAAAAAAAAAAAAAAAAAAICAAAAAAAABwMAABYAAABE
AgAAAAAAAEQCAAAAAAAARAIAAAAAAAACAgAACgAAAKgAAAAAAAAAAgIAAAAAAACoAAAAAAAAAAIC
AAAAAAAAzAIAAAAAAAAAAAAAAAAAAEQCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

...можно я тут вырежу десяток страниц этого мусора? ;-)

BAQAAAAYAAAD/////
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGoAAAAAAAAATwBiAGoAZQBj
AHQAUABvAG8AbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYA
AQD///////////////8AAAAAAAAAAAAAAAAAAAAAAAAAANBMFaNHicEB0EwVo0eJwQEAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAP///////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAEAAAD+////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////AQD+/wMKAAD/////BgkCAAAAAADAAAAAAAAARhgAAADE7urz7OXt
8iBNaWNyb3NvZnQgV29yZAAKAAAATVNXb3JkRG9jABAAAABXb3JkLkRvY3VtZW50LjgA9DmycQAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAA=

------------11552613C2F043A--

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

Не буду строить из себя умника, а отошлю вас к статье по этому адресу: http://www.bryansk.ru/pismo.html, в которой коротко и очень внятно расписывается все, что нужно знать для чтения письма в исходнике. А умение читать исходник -- неминуемо приводит к умению создавать подобное самостоятельно.


^^^   Письма с вложенными файлами (продолжение)

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

Набор наших функций мы помещаем в класс utils, договорились?

Определимся с переменными и константами.

// Статические  значения
var $mail_boundary = "----_=_NextPart_000_01C1.94F.653432C1";
var $mail_boundary2="----_=_NextPart_001_01C1.94F.653432C1"; // может пригодиться
var $mail_priority=3   // Приоритет письма 

// А это вполне переменные значения
var $mail_from;       // Отправитель
var $mail_to;          // Получатель
var $mail_subj;       // Тема письма

// Еще нам потребуется:
var $mail_body_plain  // plain-text -- обычный текст письма
var $mail_body_html   // html-формат письма
var $mail_body;      // все письмо целиком
var $attach        // вложенные файлы
var $attach_type // типы (форматы) вложенных файлов

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

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

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

function mail_header()
{
 $header="Reply-To: ".$this->mail_from. "\n";
 $header.="MIME-Version: 1.0\n";
 $header.="Content-Type: multipart/mixed; boundary=\"".$this->mail_boundary. "\"\n";
 $header.="X-Priority: ".$this->mail_priority. "\n";

 return($header);
}

По-моему, тут пояснять нечего. Про каждый параметр создаваемого заголовка вы только что прочли тут: http://www.bryansk.ru/pismo.html.

Бросили заголовок, перешли к телу письма. Тело современного электронного письма с вложениями (attachments) не редко состоит из трех(!) видимых частей. И если вы пользуетесь правильным почтовым клиентом, то вы их всегда видите:

  1. Текстовый вариант основного текста;
  2. html-вариант основного текста (почему-то почтовые клиенты отдают именно ему приоритет. Видимо, из-за всякой "красоты", которую несет html);
  3. Собственно, присоединенные файлы. Или как их называют в просторечье -- аттачи;

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

function mail_body($html, $plain)
{
 $this->mail_body_html=$html;
 $this->mail_body_plain=$plain;
}
Честно говоря, можно было этой функции вообще не писать, т.к. она производит всего лишь простое присваивание, которое можно делать точно так же перед отправкой каждого письма. Никакой разницы, вроде? Действительно, зачем вызывать для прямого присваивания дополнительную функцию. Но я предпочитаю подобные вещи оформлять отдельной функцией для того, чтобы в последствии можно было единым махом добавить что-то в систему отправки писем -- фильтры на запрещенные слова, например. Или, если ваши скрипты обслуживают клиентов, то можно в эту функцию добавить сквозную нумерацию писем -- прицеплять к тексту что-то вроде: "Идентификационный номер данного письма 00012424 от 02.12.02. Если вы нуждаетесь в дополнительных пояснениях, напишите в нашу службу поддержки, указав ID письма...", -- да и снабдить еще эту функцию логированием всех писем, чтобы знать -- когда кому и что было отправлено. И так далее. Как видите -- маленький "перезаклад" сегодня может выйти солидной экономией времени в будущем.

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

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

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

function mail_attach($name, $type, $data)
{
 $this->mail_attach_type[$name]=$type;
 $this->mail_attach[$name]=$data;
}

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

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

начало цикла

$this->mail_attach($err[$n],"text/html", "<b>Ошибка такая-то, </b><br><br>примите меры!");
конец цикла

Т.е. наши вложенные файлы будут иметь формат html и нести небольшой текст с номером ошибки. Особенно это удобно, когда делаешь "на лету" какое-нибудь преобразование. Например, я бы захотел высылать подписчиками маленькие варианты карикатур с caricatura.ru. Зачем же мне после преобразования карикатуры или нанесения на нее защитной надписи сохранять файл на диске, генерируя уникальное имя, потом читать его, а потом за собой удалять? Вот тут удобно данные передать сразу в функцию отправки.

А как быть с файлами с диска? Да очень просто.

function mail_fileattach($path, $type)
{
 $name=ereg_replace("/(.+/)/","",$path);
 if(!$r=fopen($path,'r')) return(1);
 $this->mail_attach($name, $type, fread($r,filesize($path)));
 fclose($r);

 return(0);
}

Идея понятна?

Мы передаем функции mail_fileattach путь к файлу на диске (или в Сети) и его тип (формат).

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

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

А третьей строкой вызываем написанную до этого функцию, которой передаем знакомые нам параметры, только в качестве третьего параметра зачитываем данные из файла.

Не забываем закрыть файл!

Вот тут очень важно заметить следующее. Размер памяти, в которой выполняется скрипт, может сильно не совпадать с размером файла или файлов, которые вы засовываете в письмо! Если не ошибаюсь, то по умолчанию объем выделяемой памяти под каждый скрипт не превышает 5Мб. Это зависит от вашего провайдера. Конечно, может возникнуть желание обойти это ограничение. Сие вполне возможно: открываешь сокет, соединяешься с почтовым сервером, договариваешься с ним, закидываешь в него хидеры и начинаешь методично зачитывать ему все содержимое жесткого диска. Но этот метод -- не для этого занятия.

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


^^^   Письма с вложенными файлами (финал)

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

function mail_body_create()
{
 $this->mail_body="\n\n";
 $this->mail_body.=$this->body_plain;

if(strlen($this->body_html)>0) // html-версия текста письма есть!
{
 $this->mail_body.="--".$this->boundary."\n ";
 $this->mail_body.="Content-Type: multipart/alternative; boundary=".$this->mail_boundary2."\n\n";
 $this->mail_body.=$this->mail_body_plan."\n";
 $this->mail_body.="--".$this->mail_boundary2."\n";
 $this->mail_body.="Content-Type: text/plain\n";
 $this->mail_body.="Content-Transfer-Encoding: quoted-printable\n\n";
 $this->mail_body.=$this->body_plain."\n\n";
 $this->mail_body.="--".$this->boundary2."\n";
 $this->mail_body.="Content-Type: text/html\n";
 $this->mail_body.="Conent-Transfer-Encoding: quoted-printable\n\n";
 $this->mail_body.=$this->mail_body_html\n\n";
 $this->mail_body.="--$boundary2--\n";

} else // html-версии письма нет!
{
 $this->mail_body.="--".$this->boundary."\n"; 
 $this->mail_body.="Content-Type: text/plain\n";
 $this->mail_body.="Content-Transfer-Encoding: quoted-printable\n\n";
 $this->mail_body.=$this->body_plain."\n\n--";
 $this->mail_body.=$this->boundary. "\n";
}

reset($this->attach_type);
while(list($name, $content_type)=each($this->attach_type) ) 
{
 $this->mail_body.="\n--".$this->boundary."\n";
 $this->mail_body.="Content-Type: $content_type\n";
 $this->mail_body.="Content-Transfer-Encoding: base64\n";
 $this->mail_body.="Content-Disposition: attachment;";
 $this->mail_body.="filename=\"$name\"\n\n";
 $this->mail_body.=chunk_split(base64_encode($this->atcmnt[$name]))."\n";
}
$this->mail_body.= "--".$this->boundary. "--\n";

return(0);
}

Ну вот, собственно, мы и сформировали наше письмо в переменной $this->mail_body.

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

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

$my->mail_from="roma ";
$my->mail_to="roma ";
$my->mail_subj="test message";

и отправляем письмо с помощью стандартной функции PHP mail().

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

А напоследок приведу весь текст со страницы http://www.bryansk.ru/pismo.html, на случай ее пропажи с указанного URL. Изучайте!

--- Начало цитаты ---

Что есть в письмах кроме писем?

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

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

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

Название поля Назначение
Return-Path: Служит для указания адреса возврата письма. Как правило, совпадает с адресом отправителя. Адрес заключается в угловые скобки.
Received: Содержит путь прохождения письма по почтовым серверам. Полей обычно несколько и каждое такое поле обозначает один сервер в маршруте. Серверы указываются в обратном хронологическом порядке, то есть каждый новый сервер вставляет свою подпись сверху. Формат таков:

Received:
from <домен> - отправитель,
by <домен> - получатель,
via <путь> - физический путь,
with <протокол> - почтовый протокол,
id <идентификатор> - идентификатор сообщения у получателя,
for <адрес> - финальный адрес получателя,
; <дата/время> - время получения сообщения. Обязательно только указание времени, остальная информация может быть пропущена, хотя обычно присутствует большинство из описанных данных.

Message-ID: Уникальный идентификатор сообщения. Как правило, идентификатор составляется из текущей даты, времени, адреса компьютера и некоей случайной величины. Записывается идентификатор в угловых скобках.
From: Автор сообщения. Указывается адрес электронной почты автора в угловых скобках, но нередко указывается еще и реальное имя.
Sender: Лицо или служба, отправившая сообщение, в том случае, если не совпадает со значением поля "From" Довольно редко встречающееся поле, попадается иногда в различных спэм-письмах.
To: Основной получатель. Указывается адрес электронной почты в угловых скобках. Можно указать еще и реальное имя, которое впрочем, будет с успехом проигнорировано почтовыми службами. Можно указать несколько получателей через запятую.
Cc: Carbon Copy. Дополнительные получатели, синтаксис такой же, как для поля "To".
Bcc: Blind Carbon Copy. Дополнительные получатели, адреса которых не должны быть видны другим получателям. Очень полезное поле! Используется когда, например, надо известить многих лиц об одном и том же, но не нужно, чтобы содержание вашей адресной книги попало ко всем корреспондентам. Синтаксис аналогичен полям "To" и "Cc".
Date: Дата написания письма, хотя некоторые системы выставляют дату отправки письма. В стандартной ситуации это все-таки дата написания письма, а дата отправки определяется из первого поля "Received".
Subject: Тема письма.
MIME-Version: Наличие этого поля говорит о том, что текст письма форматирован в соответствии со стандартом MIME. Значение поля говорит об используемой версии MIME. Подробное описание стандарта MIME см. в RFC 1521. Например: "MIME-Version: 1.0".
In-Reply-To: Ссылка на письмо, ответом на которое является настоящее письмо. Письма идентифицируются своими уникальными идентификаторами, проставляемыми в поле "Message-ID".
References: Ссылки, на другие письма, относящиеся к данному. Является обобщением поля "In-Reply-To".
X-Mailer: Информация о почтовой программе, использованной для написания письма.
Priority: Приоритет письма. Бывает "обычным", "срочным" и "не срочным". Как правило, используются фразы: "Normal", "Urgent", "Non-urgent". Может оказывать влияние на скорость обработки и передачи письма различными промежуточными почтовыми системами.
X-Priority: Тоже приоритет письма, но в отличие от "Priority" обозначается цифрами.
Importance: Пометка от отправителя получателю о важности сообщения. Может принимать значения "High", "Normal" или "Low". В отличие от поля "Priority" никак не влияет на скорость пересылки.
Content-Type: Формат содержимого письма. Определяется тип информации в письме и способ ее представления. В частности задается кодировка письма, если используется какой-либо национальный набор символов.
Content-Transfer-Encoding: Метод кодирования, используемый в письме, согласно стандарту MIME.

Приведенная краткая таблица не претендует на абсолютную полноту. Существует масса устаревших или, напротив, новых (экспериментальных) полей заголовка e-mail. Например, Microsoft использует поля "X-MimeOLE" и "X-MSMail-Priority", которые являются нестандартными. Строго говоря, все поля, начинающиеся с "X-" (так называемые, private-поля), не являются стандартными, а используются по внутрифирменным соглашениям. Существуют редко используемые стандартные поля, например: "Keywords", "Comments", "Content-Description" - служащие для описания содержимого письма. Но в подавляющем большинстве случаев вышеприведенной таблицы вполне достаточно для прочтения и понимания содержания заголовка. Впрочем, для полноты картины эту таблицу желательно дополнить более подробным описанием стандарта MIME (Multipurpose Internet Mail Extensions), поскольку многие поля так или иначе относятся именно к MIME, информацию по которому можно найти в RFC 1521 и RFC 1522.

Итак, MIME предназначен для задания способов передачи посредством электронной почты различной нетекстовой информации, а также больших объемов информации, которые необходимо посылать в нескольких письмах, разбив на кусочки. Следует заметить, что под нетекстовой информацией надо понимать не только бинарные файлы, но и тексты в нестандартной кодировке, то есть в отличной от ASCII, описанной в ANSI X3.4-1986.

Одно из основных полей, касающихся MIME, это "Content-Type". Возможные его значения приведены в таблице.

Название поля Назначение
text Текстовая информация в виде набора символов или описания текста на стандартном языке.
multipart Служит для объединения нескольких частей (возможно, разных типов) сообщения в одно письмо.
application Служит для пересылки бинарных файлов приложений или данных посредством электронной почты.
message Указывает на инкапсуляцию другого письма.
image Служит для пересылки изображений.
audio Служит для пересылки звуковой информации.
video Служит для пересылки видеоинформации.

В поле "Content-Type" нужно указать подтип информации. Например, для поля "text" используются в основном подтипы "plain" - означающий, что текст обычный и неформатированный, и "html" - указывающий на то, что сообщение в формате HTML. Если отправляется сообщение в формате HTML, то правильная почтовая программа создаст две части письма: в "text/plain" и "text/html", чтобы в случае непонимания подтипа "html" на получающей стороне не возникло неудобств. С другой стороны, правильная почтовая программа получателя должна показать письмо в максимально информативном виде и поэтому, пропустив "text/plain"-часть сообщения, отобразит "text/html". В общем случае, текст может быть форматирован любым способом, понимаемым обеими сторонами. Это декларировано в RFC 1341. В поле "text" также указывается кодовая таблица. Например: "Content-type: text/plain; charset=us-ascii".

Для поля "multipart" можно указывать подтипы: "alternative" - на случай, если несколько частей представляют одно и то же, а надо выбрать одну из них для отображения; "parallel" - если надо отобразить части одновременно; "digest" - если каждая часть имеет тип "message"; и некоторые другие подтипы. Для поля "message" основной подтип - "rfc822". Еще пара используемых подтипов: "partial" - для посылки части сообщения и "External-body" - для передачи, например, объемной информации путем ссылки на внешний источник данных.

Подтип поля "image" задает графический формат, в котором пересылается изображение. Основные форматы - jpeg и gif. Поле "audio" имеет основной подтип "basic". Поле "video" - основной подтип "mpeg".

Поле "application" имеет основной подтип "octet-stream" в случае обычных бинарных данных. Если посылается файл известного типа, то указывается его тип. Так, для MS Word документов Outlook пишет "application/msword", а для MS Excel "application/vnd.ms-excel".

В соответствии с RFC 822, все письма, по умолчанию, передаются простым текстом в кодировке US-ASCII, что соответствует "Content-type: text/plain; charset=us-ascii".

Следующее важнейшее поле, касающееся MIME это "Content-Transfer-Encoding". Служит оно для указания способа кодирования, в случаях, когда посылаемое письмо содержит что-либо не умещающееся в рамках US-ASCII. Стандартизованы следующие способы:

Кодировка Примечание
7bit Считается, что длина одной строки сообщения не более 1000 символов, а сами строки состоят только из US-ASCII-символов.
8bit Строки также ограничены по длине, но могут использоваться символы из старшей половины кодовой таблицы.
binary Нет ограничений ни на длину, ни на содержание.
base64 Письмо кодировано в соответствии со стандартом "base64".
quoted-printable Письмо кодировано в соответствии со стандартом "quoted-printable".

Подробно касаться методов "quoted-printable" и "base64", равно как "uuencoding" и некоторых других, в этой статье касаться не станем, поскольку тема это довольно объемистая. Разработчики почтовых систем могут использовать свои (оригинальные) способы кодирования, с тем лишь условием, чтобы принимающая сторона смогла корректно декодировать информацию.

Вторая задача MIME - это стандартизация разбиения больших писем на несколько кусочков. Для этого в поле "Content-Type" после значения "multipart/<subtype>" указывается строка - уникальный ограничитель фрагментов "boundary=<boundary string>". А затем перед каждым фрагментом пишется эта строка, предваренная двумя минусами, а в конце фрагментации еще одна, завершающаяся такими же двумя минусами.

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

  • В программах MS Outlook Express и MS Internet Mail, необходимо нажать правую кнопку мыши, когда курсор указывает на сообщение, и выбрать пункт "Properties" (либо в меню "File" также выбрать "Properties"). В появившемся диалоге надо отметить вторую закладку "Details", а потом нажать кнопку "Message Source". Появится окно, содержащее то, что нас интересует.
  • В программе Netscape Mail в меню "Options/Show Headers" нужно отметить "All"", и заголовки писем предстанут во всей своей красе.
  • В программе Netscape Messenger в меню "View/Headers" надо также выбрать "All".
  • В программе The Bat надо в меню "Вид" отметить "Служебная информация".

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

Маленькое резюме. Что же действительно интересного можно увидеть в заголовке письма?

  • Полный маршрут прохождения письма, включая время отправления в полях "Received". Например, таким образом вы сможете выяснить, по вине какого сервера ваше письмо задержалось на целых полчаса.
  • Тип почтовой программы и операционной системы отправителя в поле "X-Mailer".
  • Возможно, имя компьютера отправителя по суффиксу поля "Message-Id".
  • Возможно, IP-адрес компьютера отправителя, по полю "Message-Id" или по первому полю "Received".
  • Список всех получателей этого письма по полю "Cc". Нередко люди, посылая массовое сообщение, например, об аварии сервера, рассылают копию письма по всем, указанным в их адресной книге. Таким образом, любой получатель сможет ознакомиться с ее содержимым. Более грамотный подход заключается в использование поля "Bcc". Тогда одно письмо, как и в первом случае, уйдет ко многим адресатам, но весь список рассылки они не получат.
  • Вероятное географическое местоположение отправителя по часовому поясу, прописанному в поле "Date", и по анализу адресов в поле "Received".

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

--- Конец цитаты ---


Веб-программирование на PHP (Часть III)