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

Самоучитель PHP 4 - Котеров Д. В.

.pdf
Скачиваний:
99
Добавлен:
24.05.2014
Размер:
4 Мб
Скачать
☆

478

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

}

?>

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

Копирование объектов

Так уж устроен PHP, что в нем все переменные, в том числе и объекты (а что такое объект, как не переменная определенного класса?), всегда рассматриваются как простой набор значений и копируются целиком. Например, если у нас есть громадный массив $A и мы выполняем оператор $B=$A, то все содержимое $A будет скопировано в $B один-в-один. Возможно, это как раз то, что и требуется, но вот с объектами сложных классов все обстоит совсем иначе. Предположим, например, что мы выполнили команды:

$Obj1=new MysqlTable("test"); $Obj2=$Obj1;

$Obj1->Drop();

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

Этого, к сожалению, нельзя избежать в PHP. А именно, мы не можем никак контролировать процесс копирования объектов. И в этом — безусловная слабость PHP. Так что будьте особенно бдительны.

Ссылки и интерфейсы

Как мы знаем, в PHP оператор присваивания всегда копирует значения переменных, какой бы сложной структуры они ни были. Это же, напомню, происходит и с объектами. Что тогда получится, если мы скопируем, например, объект класса MysqlTable? Вообще говоря, ничего хорошего. Произойдет дублирование всех свойств и методов объекта. Фактически, мы получим сразу две независимые "обертки" для одной и той же таблицы MySQL. Таким образом, изменения, внесенные в первый объект, никак не повлияют на второй, и наоборот.

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

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

479

нам приходится очень часто использовать функцию GetInfo() и довольно редко — SetInfo(). Так как GetInfo() при каждом запросе обращается к MySQL, мы можем получить здесь ощутимый проигрыш в быстродействии. Очевидное решение заключается в промежуточном хранении данных, возвращаемых нашим "обычным" методом GetInfo() в специальном свойстве объекта. Действительно, зачем загружать сервер лишней работой по чтению одних и тех же данных, когда можно хранить их в программе и сразу же использовать? Это свойство будет инициализироваться при конструировании объекта класса MysqlTable и обновляться каждый раз при обращении к методу SetInfo().

То есть наше свойство будет представлять собой аналог "зеркала" записи в таблице MySQL, по аналогии с "зеркалами" сайтов в Интернете. Класс MysqlTable должен следить за тем, чтобы оно всегда содержало актуальные данные — те же самые, что и в реальной таблице.

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

$t1=new MysqlTable("MyTable");

. . .

function DoIt($t)

{ $t->SetInfo("This is the new info!");

}

. . .

$t=new MysqlTable("MyTableName"); $t->SetInfo("Data");

DoIt($t);

$Inf=$t->GetInfo(); // в $Inf будет строка Data!

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

function DoIt(&$t)

{ $t->SetInfo("This is the new info!");

}

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

480

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

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

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

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

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

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

Возврат ссылки на объект

Первый прием связан с новой возможностью PHP версии 4 — ссылочными переменными. Помните, в части III этой книги мы говорили, что функция может возвращать ссылку на переменную (объект), а не только копию переменной?.. В нашем случае это оказывается довольно удобно. Вот как могла бы выглядеть функция OpenTable() и использование для нее ссылок (листинг 31.4):

Глава 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;

}

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