Самоучитель по PHP 4
.pdfГлава 30. Код и шаблон страницы |
451 |
ob_end_clean(); // т. к. это последний буфер, буферизация отключается
//Распечатываем значения буферов, которые мы сохранили в массиве foreach($A as $i=>$t) echo "$i: $t<br>";
//Выводится:
//2: 2
//1: 11
?>
Мы видим, что схема буферизации выходного потока чем-то похожа на стек: всегда используется тот буфер, который был активизирован последним. У такой схемы довольно много положительных черт, но есть и одна отрицательная. А именно, если какая-то логическая часть программы использует буферизацию выходного потока, но по случайности "забудет" вызвать ob_end_clean() перед своим завершением, оставшаяся программа останется "в недоумении", что же произошло. К сожалению, в PHP мы никак не сможем обойти это ограничение, так что призываю вас быть особенно внимательными.
Проблемы с отладкой
В последней версии PHP на момент написания этих строк имелось небольшое неудобство, которое может превратить отладку программ, использующих буферизацию, в сущий ад. Дело в том, что при включенной буферизации все предупреждения, в нормальном состоянии генерируемые PHP, записываются в буфер, а потому (если программа не отправляет буфер в браузер) могут потеряться. К счастью, это касается лишь предупреждений, которые не завершают работу сценария немедленно. Фатальные ошибки отправляются в браузер почти всегда. Неприятность как раз и состоит в этом "почти". Даже фатальные ошибки останутся программистом незамеченными, если он вызывает функцию ob_start() вложенным образом, — т. е. более одного раза, как это было описано в предыдущем абзаце. Например, если в листинге 30.15 после присваивания текста элементу массива $A[2] вставить вызов несуществующей функции, программа сразу же выдаст пользователю текущее содержимое буфера номер 1, а затем, "не сказав ни слова", завершится. Это и понятно: ведь сообщение об ошибке попало во второй буфер, а значит, было проигнорировано! Почему разработчики PHP, вопреки общеизвестной практике, не разделили стандартный выходной поток и поток ошибок, остается неясным.
Если вы заметили, шаблонизатор всегда использует не более одного буфера "перехвата" в каждый момент времени. Это сделано именно из соображений упрощения отладки сценариев. И все же, если нефатальное предупреждение было сгенерировано в момент обработки блока, который по каким-то причинам не входит в шаблон страницы, оно останется незамеченным программистом. Впрочем, наверное, в этом нет ничего страшного: раз блок не выводится, значит, нам все равно, правильно он отработан или нет….
452 |
Часть V. Приемы программирования на PHP |
Глава 31
Объектно-ориентированное программирование на PHP
В последние 10 лет идея объектно-ориентированного программирования (ООП),
кардинально новая идеология написания программ, все более занимает умы программистов. И это неудивительно. В самом деле, сейчас происходит (а точнее, уже произошло, особенно после выхода стандарта на С++ от 98-го года и изобретения таких языков, как Java и Delphi) примерно то же, что произошло в начале 80-х годов при появлении идеи структурного программирования.
Объектно-ориентированные программы более просты и мобильны, их легче модифицировать и сопровождать, чем их "традиционных" собратьев. Кроме того, похоже, сама идея объектной ориентированности при грамотном ее использовании позволяет программе быть даже более защищенной от различного рода ошибок, чем это задумывал программист в момент работы над ней. Однако ничего не дается даром: сами идеи ООП довольно трудны для восприятия "с нуля", поэтому до сих пор очень большое количество программ (различные системы Unix, Apache, Perl, да и сам PHP) все еще пишутся на старом добром "объектно-неориентированном" Си. Что ж, очень жаль. Ощущение жалости усиливается, если посмотреть на исходные тексты этих программ, поражающие своей многословностью...
PHP, как и большинство современных языков, обеспечивает некоторую поддержку ООП. Конечно, эта поддержка далеко не полна: например, нет множественного наследования и сокрытия данных, довольно примитивен и сам механизм наследования и полиморфизма. Правда, в четвертой версии PHP наметился кое-какой прогресс: появились ссылочные переменные, но их использование все еще несколько затруднительно из-за неудобного синтаксиса. Однако это все же лучше, чем ничего.
В этой главе я кратко изложу основные идеи ООП, подкрепляя их иллюстрациями программ на PHP. Конечно, данная глава ни в коей мере не претендует на звание учебника по ООП. Интересующимся читателям рекомендую изучить любой из монументальных трудов Бьерна Страуструпа, изобретателя языка C++.
Классы и объекты
Ключевым понятием ООП является класс. Класс — это просто тип переменной. Ну, не совсем просто... На самом деле переменная класса (далее будем ее называть объ-
454 |
Часть V. Приемы программирования на PHP |
ектом класса) является в некотором смысле автономной сущностью. Обычно такой объект имеет набор свойств и операций (или методов), которые могут быть с ним проведены. Например, мы можем рассматривать тип int как класс. Тогда переменная этого "класса" будет обладать одним свойством (ее целым значением), а также набором методов (сложение, вычитание, инкремент и т. д.).
В языке C++ мы могли бы, действительно, объявить тип int именно таким образом. Однако в PHP дело обстоит немного хуже: мы не имеем права переопределять стандартные операции (сложение, вычитание и т. д.) для объектов. Например, если бы мы захотели добавить в язык комплексные числа, в C++ это можно было сделать без особых затруднений (и класс комплексных чисел по использованию практически не отличался бы от встроенного типа int), однако в PHP у нас такое добавление не удастся. Альтернативное решение состоит в том, чтобы везде вместо + и других операций использовать вызовы соответствующих функций — например, Add(), которые бы являлись методами класса.
Но обо всем по порядку. Давайте посмотрим, как создать класс в PHP. Это довольно несложно:
class MyName {
описания свойств
. . .
определения методов
}
Замечу, что здесь не создается объекта класса, а только определяется новый тип.
Чтобы создать объект класса MyName, в PHP нужно воспользоваться специальным оператором new:
$Obj = new MyName;
Вот теперь в программе существует объект $Obj, который "ведет себя" так же, как и все остальные объекты класса MyName.
Свойства объекта
Но давайте пока не будем создавать объектов, а вернемся опять к классу. Сначала (честно говоря, можно и не только в начале, но и в любом другом месте описания) должны следовать описания свойств класса. Свойство — это просто своеобразная переменная внутри объекта класса, в которой может храниться какое-то значение. Например, в классе таблицы MySQL, которым мы вскоре займемся, имя таблицы задано в виде свойства $TableName. То есть, грубо говоря, каждый объект-таблица содержит в себе свою собственную переменную $TableName и имеет над ней полный контроль. Какие именно свойства будет иметь любой объект заданного класса, указывается при создании этого класса.
Глава 31. Объектно-ориентированное программирование на PHP |
455 |
||
|
|
|
|
|
|
|
|
Мы можем представить несколько объектов одного и того же типа как братьев- близнецов: у них все одинаково с "физиологической" точки зрения (одни и те же имена свойств), но на самом деле это совершенно разные "люди" — у них разные взгляды, различный образ жизни (свойства содержат разные значе- ния).
Объект класса может напрямую обращаться к своим свойствам, считывать их или записывать. Еще раз: каждый объект одного и того же класса имеет свой собствен- ный набор значений свойств, и они не пересекаются. Таким образом, объект класса со стороны представляется контейнером, хранящим свои свойства.
Объявление свойств задается при помощи ключевого слова var:
var $pName1, $pname2, ...;
Мы видим, что каждое свойство должно иметь уникальное имя в классе. Инструкций var может быть несколько, и они могут встречаться в любом месте описания класса, а не только в его начале.
Займемся теперь вопросом о том, как нам из программы получить доступ к какому-то свойству определенного объекта (например, объекта $Obj, который мы только что создали). Это делается очень просто при помощи операции ->:
//Выводим в браузер значение свойства Name1 объекта $Obj echo $Obj->Name1;
//Присваиваем значение свойству
$Obj->Name2="PHP Four";
Если какое-то свойство (например, с именем SubObj) объекта само является объектом (что вполне допустимо), нужно использовать две "стрелочки":
//Выводим значение свойства Property объекта-свойства
//$SubObj объекта $Obj
echo $Obj->SubObj->Property;
Такой синтаксис был придуман из того расчета, чтобы быть максимально простым. Добавлю, что указание объекта $Obj перед стрелкой обязательно по той причине, что каждый объект имеет свой собственный набор свойств. Поэтому-то они и не пересекаются при хранении, а при доступе нужно уточнить объект, свойство которого запрашивается.
Впрочем, в данном простом примере объект ничем не лучше обычного ассоциативного массива — ведь мы просто используем его как контейнер для хранения свойств. Поэтому давайте поговорим о более существенном отличии — методах класса.
456 |
Часть V. Приемы программирования на PHP |
Методы
Основная идея ООП — инкапсуляция — базируется на объединении данных (свойств) объекта с функциями, которые эти данные обрабатывают. В самом деле, почему это мы привыкли разграничивать информацию и методы ее обработки? Разве, в конце концов, эти методы сами не являются информацией? Зачем же разделять неразделимые сущности?..
Фактически, свойства хранят в себе состояние объекта в данный момент времени, тогда как методы (функции обработки) являются чем-то вроде механизма посылки запроса экземпляру класса (объекту). Например, в классе таблицы MySQL, которую мы с вами вскоре напишем, может быть довольно большой набор методов. Самый простой из них — Drop(), заставляющий таблицу очистить и удалить себя из базы данных. Вызов этого метода из программы происходит примерно так:
$Obj->Drop(); // таблица $Obj удаляет сама себя!
Конечно, у методов, как и у обычных функций, могут быть параметры. К примеру, метод Add() того же класса (добавление новой записи в таблицу) принимает только один параметр — ассоциативный массив, содержащий данные, а метод Select() (получить все записи, удовлетворяющие запросу) использует три параметра — логическое выражение запроса, максимальное количество получаемых записей и правила сортировки результата. Он возвращает массив с результирующими записями.
Класс таблицы MySQL
Пожалуй, я слишком далеко заглянул в будущее. Вернемся назад к основам. Чтобы определить метод внутри класса, используется следующий синтаксис:
сlass MyClass {
. . .
function Method(параметры) { . . .
}
. . .
}
Давайте будем потихоньку набрасывать план нашего класса MySQL-таблицы. Во первых, зададимся вопросом: зачем нам вообще это нужно? Почему бы не пользоваться обычными функциями для работы с MySQL? Ответ не вполне очевиден, поэтому оставим его на потом. А пока будем считать, что такой класс нам необходим (а он действительно необходим, т. к. значительно упрощает работу с базой данных).
Во-вторых, сформулируем правило: обращаться к какой-то таблице MySQL только посредством нашего класса, а точнее, объекта этого класса, связанного с таблицей. Как же его связать? Очевидно, объект должен содержать имя таблицы, к которой он
Глава 31. Объектно-ориентированное программирование на PHP |
457 |
"привязан". Так как в программе могут использоваться одновременно несколько таблиц и несколько объектов, то, наверное, логично это самое имя хранить в виде свойства.
Что бы еще хотелось знать об объекте-таблице? Конечно, имена и типы ее полей. Поместим их в свойство-массив. Наконец, в процессе работы наверняка иногда будут возникать ошибки. Чтобы как-то сигнализировать о них, предлагаю в класс-таблицу ввести еще одно свойство — Error. Оно будет равно нулю, если предыдущая операция (например, добавление записи) прошла успешно, и тексту ошибки — в противном случае.
Вот что у нас пока получилось:
class MysqlTable {
var $TableName; // Имя таблицы в базе данных
var $Fields; // Массив полей. Ключ — имя поля, значение — его тип var $Error; // Индикатор ошибки
. . .
}
Согласитесь, это почти все данные, которые должны храниться в объекте-таблице. Все остальное (например, записи) находится в базе данных. Нам нужно научиться каким-то образом легко извлекать и добавлять (а также удалять, подсчитывать и обновлять) эти записи путем простых запросов к объекту-таблице. Для этого я предлагаю написать соответствующие методы (листинг 31.1).
Пока мы не будем расписывать код методов. Взамен просто обозначим его словом "команды" в тексте программы. Вообще говоря, такой способ проекти- рования, когда сначала решают, какие методы нам нужны, а потом начинают продумывать их код, довольно типичен для ООП.
Листинг 31.1. Эскиз класса таблицы
class MysqlTable {
var $TableName; // Имя таблицы в базе данных
var $Fields; // Массив полей. Ключ — имя поля, значение — его тип var $Error; // Индикатор ошибки
//Добавляет в таблицу запись $Rec. $Rec должна представлять из себя
//обычный ассоциативный массив. В будущем мы придем к тому, что
//массив $Rec будет представлен даже древовидной структурой,
//т. е. будет иметь подмассивы.
//Как вы понимаете, непосредственной поддержки этого в MySQL нет,
//но мы "ее" реализуем.
458 |
Часть V. Приемы программирования на PHP |
function Add($Rec) { команды; }
//Возвращает массив записей (ключ — id записи, значение —
//ассоциативный массив, в точности такой же, какой был помещен
//некогда в таблицу при помощи Add), удовлетворяющих выражению
//$Expr. Возвращаются только первые $Num (или менее) записей.
//Сортировка осуществляется в соответствии с критерием $Order. function Select($Expr,$Num=1e10,$Order="id desc") { команды; }
//Удаляет из таблицы все записи, удовлетворяющие выражению $Expr. function Delete($Expr) { команды; }
//Удаляет из таблицы все записи (например, при помощи вызова
//Delete("1=1") и удаляет саму таблицу из базы данных. Этот
//метод довольно опасен!
function Drop() { команды; }
}
Пока, пожалуй, хватит. Я не буду здесь углубляться в то, как устроен каждый из названных методов. Этим мы займемся в свое время. А пока обратите внимание на то, что мы попытались определить все операции, которые вообще применимы к таблице MySQL (на самом деле, это далеко не полный их перечень, но пока нам и такого количества вполне достаточно). Это очень важно, потому что потом, когда будем использовать объекты класса MysqlTable, мы сможем вообще забыть, что такое MySQL и язык запросов SQL, или поручить разработку программы, обращающейся к MysqlTable, человеку, не разбирающемуся в SQL.
Вообще говоря, это один из самых главных приемов ООП (структурного программирования — в меньшей степени) — постоянно размышлять, как бы нам сделать так, чтобы потом можно было побольше "забыть". Работает принцип: если вы используете какой-то класс и не догадываетесь, как он реализован, причем это вам нисколько не мешает, значит, класс хорош. И наоборот. Впрочем, совсем абстрагироваться от SQL нам все же не удастся — всетаки нужно знать правила составления выражений для выборки и удаления записей, для их сортировки и т. д. Но это уже не SQL, а что-то гораздо более простое и интуитивно понятное.
Доступ объекта к своим свойствам
Как ни странно, но при изучении ООП "с нуля" программисты, привыкшие к структурному программированию, часто с трудом понимают, каким образом объект может добраться до своих собственных свойств. Рассмотрим, например, такую программу:
$Obj1=new Mysqltable;
Глава 31. Объектно-ориентированное программирование на PHP |
459 |
$Obj2=new MysqlTable;
. . .
echo $Obj1->TableName, " ", $Obj2->TableName;
Здесь никаких проблем не возникает — ясно, что выводятся свойства разных объектов — мы же сами указали их до стрелки. Однако давайте посмотрим, что будет, если вызвать какой-нибудь метод одного из объектов:
$Obj1->Drop();
Как видите, при вызове метода так же, как и при доступе к свойству, нужно указать объект, который должен "откликнуться на запрос". Действительно, этой командой мы удаляем из базы данных таблицу $Obj1, а не $Obj2. Рассмотрим теперь тело метода
Drop():
class MysqlTable { function Drop()
{ сюда интерпретатор попадет, когда вызовется Drop() для какого-то объекта
}
}
По логике, Drop() — функция. Эта функция, конечно, едина для всех объектов класса MysqlTable. Но как же метод Drop() узнает, для какого объекта он был вызван? Ведь мы не можем Drop() для $Obj1 сделать одним, а для $Obj2 — другим, иначе нарушился бы весь смысл нашей объектной ориентированности. В том-то вся и соль, что два различных объекта-таблицы являются объектами одного и того же класса...
Оказывается, для доступа к свойствам (и методам, т. к. один метод вполне может вызывать другой) внутри метода используется специальная предопределенная переменная $this, содержащая тот объект, для которого был вызван метод. Теперь мы можем определить Drop() внутри класса так:
function Drop()
{ // сначала удаляем все записи из таблицы $this->Delete("1=1"); // всегда истинное выражение
// а затем удаляем саму таблицу mysql_query("drop table ".$this->TableName);
}
Если мы вызвали Drop() как $Obj1->Drop(), то $this будет являться тем же объектом, что и $Obj1 (это будет ссылка на $Obj1), а если бы мы вызвали $Obj2- >Drop(), то $this был бы равен $Obj2. То есть метод всегда знает, для какого объекта он был вызван. Это настолько важно, что я повторю еще раз: метод всегда знает, для какого объекта он был вызван.
Использование ссылок говорит о том, что $this — не просто копия объекта-хозяина, это и есть хозяин. Например, если бы в $Obj1->Drop() мы захотели изменить ка-
460 Часть V. Приемы программирования на PHP
кое-то свойство $this, оно поменялось бы и у $Obj1, но не у $Obj2 или других объектов.
В синтаксисе PHP есть один просчет: запись вида $ArrayOfObjects["obj"]->DoIt();
считается синтаксически некорректной. Вместо нее применяйте следующие две команды:
$obj=&$ArrayOfObjects["obj"]; $obj->DoIt();
Не забудьте про & сразу после оператора присваивания (то есть создавайте ссылку на элемент массива), иначе метод DoIt() будет вызван не для самого объекта, присутствующего в массиве, а для его копии, полученной в $obj!
Инициализация объекта. Конструкторы
До сих пор мы не особенно задумывались, каким образом были созданы объекты $Obj1 и $Obj2 и к какой таблице они прикреплены. Однако вполне очевидно, что эти объекты не должны существовать сами по себе — это просто не имеет смысла. Поэтому нам, наравне с уже описанными методами, придется написать еще один — а именно, метод, который бы:
r"привязывал" только что созданный объект-таблицу к таблице в MySQL;
rсбрасывал индикатор ошибок;
rзаполнял свойство Fields;
rделал другую работу по инициализации объекта.
Назовем это метод, например, Init():
class MysqlTable {
. . .
// Привязывает объект-таблицу к таблице с именем $TblName function Init($TblName)
{ $this->TableName=$TblName; $this->Error=0;
получаем и заполняем $this->Fields
}
}
. . .
$Obj=new MysqlTable; $Obj->Init("test");