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

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

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

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