Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Самоучитель по PHP 4

.pdf
Скачиваний:
82
Добавлен:
02.05.2014
Размер:
4.36 Mб
Скачать

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

481

Листинг 31.4. Использование ссылок

//Массив всех уже открытых таблиц. Ключи — имена таблиц, значения —

//соответствующие объекты.

$Tables=array();

. . .

//Функция OpenTable() возвращает ссылку на объект, соответствующий

//таблице MySQL с заданным именем. Копии объектов не создаются. function &OpenTable($name,$Fields="")

{ global $Tables; if(!Isset($Tables[$name]))

$Tables[$name]=new MysqlTable($name,$Fields); return $Tables[$name];

}

. . .

//Вот так мы должны использовать эту функцию.

$Tbl1=&OpenTable("MyTable"); // создает новый объект $Tbl2=&OpenTable("OtherTable"); // создает объект $TblEqualsTo1=&OpenTable("MyTable"); // возвращает имеющийся объект!

//Теперь $Tbl1 и $TblEqualsTo1 ссылаются на один и тот же объект.

//То есть изменение $Tbl1 тут же отразится на $TblEqualsTo1,

//и наоборот.

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

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

rУ неопытного программиста, использующего ваш класс, может возникнуть искушение скопировать $Tbl1 в новую переменную "обычным" образом — при помощи оператора =. Или же он может по ошибке пропустить &, когда объявляет функцию со ссылочным параметром.

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

482

Часть V. Приемы программирования на PHP

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

Возврат интерфейса

Поговорим немного о том, что же собой представляют интерфейсы в объектноориентированном программировании. Это понятие довольно сложное, и о нем написано множество томов. Я, разумеется, не собираюсь их здесь пересказывать, потому что эта книга — о PHP, а не об идеологии ООП.

Интерфейсы — главная "изюминка" практически всех сложных объектноориентированных систем (например, COM+, CORBA) и одно из основных понятий такого языка, как Java. Язык C++ также во всем поддерживает эту идеологию. Что же может дать нам PHP в этом отношении? К сожалению, довольно немного. И все-таки даже этого хватает, чтобы избавиться от недостатков, присущих ссылкам в PHP — во всяком случае, для нашей задачи.

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

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

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

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

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

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

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

483

 

 

 

 

 

 

 

 

Вроде бы понятно, не правда ли? А теперь давайте уберем все, кроме слов- связок, но оставим курсив. Вот что у нас получится. "Представим себе объект большого и сложного класса, например, MysqlTable, расположенный в опе- ративной памяти. Мы не хотим использовать свойства или методы объекта на- прямую. Если уж быть честными, мы даже не можем напрямую использовать в программе этот объект на него нет ссылок, и уж подавно не способны ско- пировать объект в другую переменную. Но зато, как мы знаем, его постоянно "сопровождают" объекты-интерфейсы, имеющие код небольшого размера. Эти интерфейсы могут передавать запросы на обслуживание разумеется, из тех, для которых имеют соответствующие методы. Конечно, объект может иметь несколько разных интерфейсов. Важно то, что мы не можем напрямую исполь- зовать объект. При этом мы имеем право копировать объекты-интерфейсы главному объекту нет до этого ровным счетом никакого дела. В PHP объект "не знает", сколько у него интерфейсов и как они используются".

Итак, основная идея такова: отделим интерфейс MysqlTable от его реализации, т. е. напишем класс IMysql, с которым и будем всегда работать. Этот класс должен содержать все те методы, которые поддерживаются MysqlTable, только заниматься они будут ни чем иным, как просто переадресацией вызовов на "настоящие" объекты. А последние, в свою очередь, хранятся в глобальном массиве объектов, на элементы которого должно ссылаться одно из свойств IMysql. Реализуем эту стратегию для упрощенной версии MysqlTable, имеющей только метод Drop() и конструктор

(листинг 31.5):

Листинг 31.5. Упрощенный интерфейс к таблице MySQL

//Массив объектов-таблиц, созданных в программе $GLOBALS["Tables"]=array(); // вначале массив пуст

//Реализация класса. Это — обычный класс без каких-либо особенностей.

//Давайте предположим, что объекты этого класса недопустимо

//копировать обычным способом.

class MysqlTable { // . . .

function MysqlTable($name) { echo "MysqlTable($name)<br>"; } function Drop() { echo "Drop()<br>"; }

}

// Класс-интерфейс class IMysql {

var $id; // идентификатор реализации таблицы (MysqlTable) в $Tables

//Открывает таблицу с именем $name. Если эта таблица уже была

//открыта ранее, то ничего не делает и просто становится ее

//синонимом, иначе создает экземпляр объекта.

484

Часть V. Приемы программирования на PHP

function IMysql($name) { global $Tables; $this->id=$name;

//Если объект для таблицы $name еще не создан, создать его if(!isset($Tables[$name])) $Tables[$name]=new MysqlTable($name);

//Иначе объект уже существует и ничего делать не надо

}

// Уничтожает таблицу. Переадресуем вызов реализации

function Drop() { $obj=&$GLOBALS['Tables'][$this->id]; $obj->Drop(); }

}

// Демонстрация работы с интерфейсом

$m=new IMysql("TestTable"); // объект создается

$m=new IMysql("TestTable"); // новый объект не создается! $m->Drop(); // очищается единственный объект

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

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

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

Хочу заметить, что в настоящих объектно-ориентированных языках нет причин прибегать к столь странным ухищрениям, потому что в них есть такое понятие, как ука- затель. В этих языках подобъект класса-интерфейса IMysql содержится прямо внутри объекта MysqlTable, и указатель на него можно получить либо посредством явных преобразований типов, либо с помощью специальных функций для "отпочковывания" интерфейса. Например, в COM+ эти функции часто называют QueryInterface(). Здесь же у нас вышло нечто вроде примитивной поддержки

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

485

указателей (ведь объект класса IMysql именно указывает на "хозяина" типа MysqlTable, но не содержит его в себе!), которых в PHP нет.

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

Глава 32

Почтовые

шаблоны

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

Мини-шаблонизатор

Конечно, пользователю будет приятно, если письмо (пусть даже и сгенерированное программой) будет адресовано ему лично. Например, в поле From содержится фамилия и имя клиента, а первые строки текста звучат как-нибудь вроде: "Уважаемый ФИО!". Так что нам придется формировать текст письма "на лету" — проставлять в нем нужное имя, фамилию, тему и т. д. по общему шаблону.

В идеале такой шаблон должен ничем не отличаться от небольшого PHP-сценария с тэгами <? и ?> и возможностью использования команды echo или print, не говоря уж о всех остальных инструкциях. Но вот беда: как нам этот самый шаблон "развернуть", превратить в письмо-строку, которую потом мы будем посылать по почте? Пусть, например, у нас есть следующий шаблон письма (разделителем заголовков и тела письма служит маркер ~StartOfMail, обрабатываемый функцией

PostMail()):

To: "<?=$Name?>" <<?=$email?>> Subject: <?=$Subject?> ~StartOfMail

Дорогой <?=$Name?>!

Только что Вы подписались на наш лист рассылки.

Пожалуйста, подтвердите свое желание получать новости нашего сайта.

Если бы мы писали сценарии на PHP версии 3, задача обработки такого шаблона была бы практически невыполнимой. К счастью, при использовании PHP версии 4 все проще: в нем имеются функции "перехвата" стандартного выходного потока (о них мы уже говорили в главе 30).

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

487

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

Листинг 32.1. Функции обработки шаблонов: Minitemplate.phl

<?

//Эта функция используется для внутренних целей. Она возвращает

//"развернутый" шаблон $templ. Перед обработкой создаются переменные,

//имена которых содержатся в ключах массива $Vars, а значения — в

//соответствующих значениях массива. Если $Vars===false, то вместо

//него используется массив $GLOBALS (то есть делаются доступными все

//глобальные переменные). Значение параметра $ReadFile "истина"

//указывает, что в $templ хранится не содержимое шаблона, а имя файла,

//из которого его можно получить.

//Замечание: параметр $Vars передается по ссылке, т. к. для

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

//копирование.

function _RunTemplate($tmpl, $ReadFile, &$Vars)

{// Перехватываем стандартный поток вывода ob_start();

//Если $Vars опущен, использовать вместо него $GLOBALS. Мы

//используем ссылки для убыстрения работы, чтобы PHP не пришлось

//копировать значения, чем экономим время.

if($Vars===false) $Vars=&$GLOBALS;

//Делаем доступными коду шаблона все переменные. Также создаем

//ссылки из соображений производительности.

foreach($Vars as $k=>$v) $$k=&$Vars[$k];

//Включаем файл по include, либо же запускаем eval(). if($ReadFile) { include $tmpl; }

else eval("?>$tmpl;<?");

//Получаем содержимое буфера и закрываем его

$MTResult=ob_get_contents(); ob_end_clean();

// Возвращаем развернутый шаблон return $MTResult;

}

// Функция "разворачивает" шаблон, тело которого расположено

488

Часть V. Приемы программирования на PHP

//в файле $fname. Перед запуском переменные из $Vars делаются

//доступными шаблону (если этот параметр не опущен). function ExpandFile($fname,$Vars=false)

{ return _RunTemplate($fname,true,$Vars);

}

//Функция "разворачивает" тело шаблона, явно заданное в $tmpl.

//Рекомендуется везде, где можно, применять ExpandFile() вместо

//данной функции, потому что это упрощает отладку.

function ExpandTemplate($tmpl,$Vars=false) { return _RunTemplate($tmpl,false,$Vars);

}

?>

Зачем нам две различных функции для "раскрытия" шаблона ExpandTemplate() и ExpandFile()? Почему бы не использовать всегда ExpandTemplate(), предварительно загружая тело шаблона с помощью функций чтения файлов? Все дело в тонкостях обработки ошибочных ситуаций в PHP. А именно, в случае ошибки внутри файла, загружаемого по include, PHP сообщит нам имя этого файла. Если же ошибка произойдет в eval(), выведется только номер строки, что сильно затруднит отладку. Поэтому реко- мендуется везде, где это допустимо, вызывать функцию ExpandFile().

Отправка и перекодирование писем

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

rвставку заголовка From в письмо, если он еще не присутствует в сообщении;

rпреобразование письма в нужную кодировку кириллицы;

rвставку соответствующего значения в заголовок Content-type, чтобы письмо было "понятно" любой почтовой программе;

rподдержку функций мини-шаблонизатора, который мы уже написали.

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

Minitemplate.phl.

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

489

Листинг 32.2. Функция PostMail(): Mail.phl

<?

Uses("Minitemplate");

//Кодировка по умолчанию для исходного текста. define("DefaultCode","w");

//Функция возвращает строку $st, переведенную из кодировки

//$from в кодировку $to. Возможные значения этих параметров:

//w[indows] — windows-1251

//k[oi8-r] — koi8-r

//m[ac] — x-mac-cyrillic

//i[so] — iso-8859-5

//t[ranslit] — translit ("английскими" буквами — "русские" слова)

//Замечание: квадратными скобками помечены необязательные символы.

//параметр $from не может равняться "t", потому что трудно

//восстанавливать текст из транслита (хотя эта задача и разрешима).

//Функция полезна и сама по себе, но все-таки чаще всего ее

//применяют для работы с почтой. Именно поэтому я включаю

//ее в этот модуль.

function EncodeString($st,$to,$from=DefaultCode)

{// Оставляем только первые буквы названий кодировок $from=strtolower(substr($from,0,1));

$to =strtolower(substr($to,0,1));

//Пытаемся воспользоваться встроенной в PHP функцией if($to!="t") return convert_cyr_string($st,$from,$to);

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

//делать "вручную" — при помощи strtr().

//Сначала заменяем "односимвольные" фонемы.

$st=strtr($st,"абвгдеёзийклмнопрстуфхъыэ", "abvgdeeziyklmnoprstufh'ie"); $st=strtr($st,"АБВГДЕЁЗИЙКЛМНОПРСТУФХЪЫЭ", "ABVGDEEZIYKLMNOPRSTUFH'IE");

// Затем — "многосимвольные". $st=strtr($st,array(

"ж"=>"zh", "ц"=>"ts", "ч"=>"ch", "ш"=>"sh", "щ"=>"shch","ь"=>"", "ю"=>"yu", "я"=>"ya", "Ж"=>"ZH", "Ц"=>"TS", "Ч"=>"CH", "Ш"=>"SH",

490

Часть V. Приемы программирования на PHP

"Щ"=>"SHCH","Ь"=>"", "Ю"=>"YU", "Я"=>"YA" )); // Возвращаем результат.

return $st;

}

//Значения параметра Content-tyep charset в зависимости от

//односимвольного названия кодировки.

global $CoderCharset; $CoderCharset["w"]="windows-1251"; $CoderCharset["i"]="iso-8859-5"; $CoderCharset["k"]="koi8-r"; $CoderCharset["m"]="x-mac-cyrillic"; $CoderCharset["t"]="koi8-r";

//Разделитель тела и заголовков (таких как From: и т. д.) в письме. define("MailDivider","~StartOfMail");

//Посылает письмо $msg по заданному адресу $to, перед этим

//преобразовав его в кодировку $encTo. Проставляет поле

//charset и правильно обрабатывает имя получателя (если

//в теле письма уже указано "To: Вася", то в результате

//получается "To: Вася <vasya@pupkin.ru>"). Если работа происходит

//в Win32, то письмо не посылается, а создается отладочный файл,

//в котором будет содержаться текст письма.

//Письмо должно состоять из заголовков и тела, разделенных

//маркером ~StartOfMail.

function SendMail($to,$msg,$encTo=DefaultCode,$encFrom=DefaultCode)

{global $CoderCharset; // Перекодируем

$msg=EncodeString($msg,$encTo,$encFrom); // тело письма $head=""; // заголовки

// Если есть заголовки, выделяем их. if(strpos($msg,MailDivider)!==false) {

$regs=split(MailDivider."\r?\n?",$msg,2); // тело и заголовки $head=trim($regs[0]);

$msg=$regs[1];

}