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

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

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

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

461

А вдруг между вызовами new и Init() случайно произойдет обращение к таблице? Или кто-то по ошибке забудет вызвать Init() для созданного объекта (что обязательно случится, дайте только время)? Это приведет к непредсказуемым последствиям. Поэтому, как и положено в ООП, мы можем завести метод вместо Init(), который будет вызываться автоматически сразу же после инструкции new и проводить работы по инициализации объекта. Он называется конструктором, или инициализатором. Чтобы PHP мог понять, что конструктор следует вызывать автоматически, ему (конструктору) нужно дать то же имя, что и имя класса. В нашем примере это будет выглядеть так:

class MysqlTable {

function MysqlTable($TblName)

{ команды, ранее описанные в Init();

}

}

$Obj=new MysqlTable("test"); // создаем и сразу же инициализируем объект

Обратите внимание на синтаксис передачи параметров конструктору. Если бы мы случайно пропустили параметр test, PHP выдал бы сообщение об ошибке. Таким образом, теперь в программе потенциально не могут быть созданы объекты-таблицы, ни к чему не привязанные.

Деструктор

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

Наследование

Создание самодостаточных объектов — довольно неплохая идея. Однако это далеко не единственная возможность ООП. Сейчас мы займемся наследованием — одним из основных понятий ООП.

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

462

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

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

class A {

function TestA() { ... } function Test() { ... }

}

class B {

var $a; // объект класса A

function B(параметры_для_A, другие_параметры) { $a=new A(параметры_для_A);

инициализируем другие поля B

}

function TestB() { ... } function Test() { ... }

}

Поясню: в этой реализации объект класса B содержит в своем составе подобъект класса A в качестве свойства. Это свойство — лишь "частичка" объекта класса B, не более того. Подобъект не "знает", что он в действительности не самостоятелен, а содержится в классе B, поэтому не может предпринимать никаких действий, специфичных для этого класса.

Но вспомним, что мы хотели получить расширение возможностей класса A, а не нечто, содержащее объекты A. Что означает "расширение"? Лишь одно: мы бы хотели, чтобы везде, где допустима работа с объектами класса A, была допустима и работа с объектами класса B. Но в нашем примере это совсем не так.

rМы не видим явно, что класс B лишь расширяет возможности A, а не является отдельной сущностью.

rМы должны обращаться к "части A" класса B через $obj->a->TestA(), а к членам самого класса B как $obj->TestB(). Последнее может быть довольно утомительным, если, как это часто бывает, в B будет использоваться очень много методов из A и гораздо меньше — из B. Кроме того, это заставляет нас постоянно помнить о внутреннем устройстве класса B.

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

(или расширение возможностей) классов.

class B extends A {

function B(параметры_для_A, другие_параметры) { $this->A(параметры_для_A);

инициализируем другие поля B

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

463

}

function TestB() { ... } function Test() { ... }

}

Ключевое слово extends говорит о том, что создаваемый класс является лишь "расширением" класса A, и не более того. То есть B содержит те же самые свойства и методы, что и A, но, помимо них и еще некоторые дополнительные, "свои".

Теперь "часть A" находится прямо внутри класса B и может быть легко доступна, наравне с методами и свойствами самого класса B. Например, для объекта $obj класса B допустимы выражения $obj->TestA() и $obj->TestB(). Итак, мы видим, что, действительно, класс B является воплощением идеи "расширение функциональности класса A". Обратите также внимание: мы можем теперь забыть, что B унаследовал от A некоторые свойства или методы — снаружи все выглядит так, будто класс B реали-

зует их самостоятельно.

Немного о терминологии: принято класс A называть базовым, а класс B про- изводным от A. Иногда базовый класс также называют суперклассом, а произ-

водный подкласcом.

Зачем может понадобиться наследование? Например, мы написали класс Mysqlтаблицы и хотели бы дополнительно иметь класс Guestbook (гостевая книга). Очевидно, в классе Guestbook будет много методов, которые нужны для того же, что и методы из MysqlTable, поэтому было бы разумным сделать его производным от

MysqlTable:

class Guestbook extends MysqlTable {

. . .

методы и свойства, которых нет в MysqlTable и которые относятся к гостевой книге

}

Многие языки программирования поддерживают множественное наследование (то есть такое, когда, скажем, класс B наследует члены не одного, а сразу нескольких классов — например, A и Z). К сожалению, в PHP таких возможностей нет.

Полиморфизм

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

464

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

Вернемся к нашему предыдущему примеру с классами A и B.

class A {

//Выводит, функция какого класса была вызвана function Test() { echo "Test from A\n"; }

//Тестовая функция — просто переадресует на Test() function Call() { Test(); }

}

class B extends A {

// Функция Test() для класса B

function Test() { echo "Test from B\n"; }

}

$a=new A();

$b=new B();

Давайте рассмотрим следующие команды:

$a->Call(); // напечатается "Test from A" $b->Test(); // напечатается "Test from B" $b->Call(); // Внимание! Напечатается "Test from B"!

Обратите внимание на последнюю строчку: вопреки ожиданиям, вызывается не функция Test() из класса A, а функция из класса B! Складывается впечатление, что Test() из B просто переопределила функцию Test() из A. Так оно на самом деле и есть. Функция, переопределяемая в производном классе, называется виртуальной.

Механизм виртуальных функций позволяет нам, например, "подсовывать" функциям, ожидающим объект одного класса, объект другого, производного, класса. Еще один классический пример — класс, воплощающий собой свойства геометрической фигуры, и несколько производных от него классов — квадрат, круг, треугольник и т. д. Базовый класс имеет виртуальную функцию Draw(), которая заставляет объект нарисовать самого себя. Все производные классы-фигуры, разумеется, переопределяют эту функцию (ведь каждую фигуру нужно рисовать по-особому). Также у нас есть массив фигур, причем мы не знаем, каких именно. Зато, используя полиморфизм, мы можем, не задумываясь, перебрать все элементы массива и вызвать для каждого из них метод Draw() — фигура сама "решит", какого она типа и как ее рисовать.

В нашем классе MysqlTable, который мы еще только-только наметили, идея полиморфизма найдет свое применение. И вот зачем. Мы проектируем класс так, чтобы другие классы, которые он будет использовать, подключали его к себе как производный. Тем самым они наследуют все свойства MysqlTable и добавляют некоторые свои. Например, класс Guestbook, реализующий гостевую книгу, может быть производным от MysqlTable и "расширять" его некоторыми дополнительными функциями — например, проверкой орфографии во введенном сообщении или же контролем, имеет ли право тот или иной пользователь писать в книгу (или он "отключен" за использование ненормативной лексики). Кроме того, прежде чем помещать данные в

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

465

MySQL-таблицу, наверное, разумным будет их немного "почистить" — убрать лишние пробелы, HTML-тэги и т. д. Конечно, такой корректировке должны быть подвержены все поля книги. Поэтому класс MysqlTable перед помещением очередной записи в таблицу будет вызывать виртуальную функцию PreModify(), передавая ей в параметрах запись, которая должна быть откорректирована. Естественно, в классе Guestbook эта функция должна переопределяться — так, чтобы выполнять требуемые действия по коррекции записи перед ее занесением в таблицу. Конечно, класс MysqlTable не "знает", как именно будет переопределена PreModify() в производном от него классе, поэтому сам он содержит функцию PreModify(), не делающую ничего (то есть с пустым телом).

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

Полноценный класс таблицы MySQL

Я ранее обещал, что в каждой главе части V книги обязательно будет присутствовать пример нетривиального кода на PHP, который (или идеи из которого) вы сможете использовать в своих программах. На этот раз "исходник" оказался особенно большим, но это с лихвой оправдывается его функциональностью. Сейчас мы с вами разработаем полноценный класс, который существенно облегчает работу с таблицей MySQL, в значительной степени абстрагируя программиста не только от специфики этой СУБД, но и вообще от сложностей SQL-запросов. С помощью этого класса даже начинающий программист сможет построить форум, гостевую книгу, да и вообще любую программу, которая требует структурированного хранилища данных большого объема. Правда, для того, чтобы извлекать максимальную выгоду из использования класса, придется разобраться в механизме наследования, вкратце описанном чуть выше. Впрочем, класс прекрасно работает и сам по себе. Вот его некоторые отличительные черты.

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

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

466

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

подвергаются сериализации, а при чтении из таблицы — восстановлению, "прозрачно" для вызывающей программы.

r В то же время имеется возможность добавления/удаления несущих столбцов "на лету", т. е. без какого бы то ни было специального запроса пользователя. Достаточно изменить список несущих полей при создании/открытии таблицы. Класс сам определяет, что именно изменилось, и применяет соответствующие действия по корректировке (вызывает нужные команды SQL).

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

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

rДля каждой таблицы можно хранить один дополнительный блок информации любой структуры (например, это может быть даже многомерный ассоциативный массив). Выборка и запись этого блока осуществляются методами GetInfo() и SetInfo(). Блок информации нельзя получить никак иначе, кроме как посредством этих двух функций (он "не виден" даже для функции выборки).

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

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

Согласитесь, не так уж и мало для каких-то четырехсот строчек кода.….. Листинг 31.2 представляет собой исходный текст библиотеки, реализующей наш класс. Она предполагает, что соединение с MySQL уже открыто и выбрана верная текущая база данных.

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

467

Листинг 31.2. Полноценный класс MySQL-таблицы

<?

//MysqlTable — "прозрачная работа" с таблицей MySQL.

//Класс MysqlTable обычно делают базовым для какого-нибудь

//другого класса (например, CGuestBook), и переопределяют

//нужные функции.

//Поле для хранения сериализованных полей (снаружи "не видно") define("DataField","__data__");

//******************* Вспомогательные функции *******************

//Если переменная пуста, инициализирует ее

function Def0(&$st,$def) { if(!isSet($st)||$st=="") $st=$def; }

//Подготавливает строку двоичных данных для помещения в таблицу. function Apostrophs(&$st)

{ $st=str_replace(chr(0),"\\0",$st); $st=ereg_replace("\\\\","\\\\",$st); $st=ereg_replace("'","\\'",$st); return $st;

}

//Упаковывает объект и превращает его в строку.

function SqlPack(&$obj)

{ $s=Serialize($obj); return Apostrophs($s); } // Распаковывает строку и создает объект.

function SqlUnpack(&$st) { return Unserialize($st); } //**************************************************************** //*** Далее идет описание класса таблицы.

//Каждая запись таблицы, помимо тех полей, которые указаны в

//конструкторе, будет иметь еще два поля — id (уникальный

//идентификатор записи) и __data__ (упакованный массив

//всех остальных полей). Кроме того, в запись можно вводить

//произвольные поля — они тоже будут сохраняться, но по

//ним нельзя будет вести поиск (предложение "select"),

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

//добавлении/изменении записи и распакованы при извлечении. class MysqlTable {

//*** Внутренние переменные var $TableName; // имя таблицы

var $UniqVars; // список уникальных полей (имя=1, имя=1...)

var $Index; // для этих полей построены индексы (имя=1, имя=1...)

468

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

var $Fields; // все физические поля таблицы (имя=тип, имя=тип...) var $Error; // текст последней ошибки ("", если нет)

var $JustCreated; // 1, если таблица была создана, а не загружена //*** Внутренние функции

//Упаковывает поля массива в строку, за исключением тех, которые

//сами являются непосредственными полями в базе данных. function _PackFields(&$Hash)

{ $Data=array();

foreach($Hash as $k=>$v) if($k!=DataField) if(!isSet($this->Fields[$k])) $Data[$k]=$v; return Serialize($Data);

}

//Виртуальная функция производного класса вызывается ПЕРЕД любым

//занесением данных в таблицу (добавлением и обновлением). То есть

//она предназначена для "прозрачной" автоматической генерации некоторых

//полей записи (например, времени ее изменения) в производном классе

//перед ее сохранением.

//Можно, к примеру, в таблице держать какую-нибудь дату в формате

//SDN, а "делать вид", что она хранится в обычном представлении

//"дд.мм.гггг".

//Если эта функция возвратит 0, то операция закончится с ошибкой. function PreModify(&$Rec) { return 1; }

//Виртуальная функция вызывается ПОСЛЕ выборки записи из таблицы, а

//также в конце модификации записи. То есть она предназначена для

//"прозрачной" модификации только что полученной из таблицы записи.

//Возвращаясь к предыдущему примеру, мы можем при извлечении записи

//из таблицы STM-поле преобразовать в "дд.мм.гггг", и "никто ничего

//не заметит".

function PostSelect(&$Rec) { return; } // Возвращает имя таблицы

function GetTableName() { return $this->TableName; }

//Возвращает результат запроса select. В дальнейшем этот результат

//(дескриптор) будет, скорее всего, обработан при помощи GetResult().

//$Expr — выражение SQL, по которому будет идти выборка

//$Order — правила сортировки (по умолчанию — по убыванию id) function TableSelectQuery($Expr="",$Order="id desc")

{ $this->Error=""; if(!$Expr) $Expr="1=1";

$r=mysql_query("select * from ".$this->TableName.

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

469

" where ($Expr) and (id>1) order by $Order"); if(!$r) { $this->Error=mysql_error(); return; } return $r;

}

function SelectQuery($Expr="",$Order="id desc") { return $this->TableSelectQuery($Expr,$Order); }

//Возвращает результат предыдущего запроса select (точнее, очередную

//найденную запись) в виде распакованного (!) массива. Если

//SelectQuery() нашла несколько записей, то, последовательно вызывая

//GetResult(), можно считать их все. Метод делает всю "черную" работу

//по сериализации. Еще раз: если у результата несколько строк, то метод

//возвращает очередную. Если строки кончились, возвращает "".

//Чаще всего в вызове этой функции (и функции SelectQuery) нет

//необходимости — можно воспользоваться методом Select(), который по

//запросу сразу возвращает массив со всеми обработанными результатами! function TableGetResult($r)

{ $this->Error="";

//Выбираем очередную строку в виде массива

if($r) $Result=mysql_fetch_array($r); else $this->Error=mysql_error(); if(!@is_array($Result)) return;

//Перебираем все поля таблицы и записываем их в массив $Hash $Hash=array();

foreach($this->Fields as $k=>$i) if(isSet($Result[$k])) $Hash[$k]=$Result[$k];

//Распаковываем поле с данными

$Hash+=SqlUnpack($Hash[DataField]); unSet($Hash[DataField]); $this->PostSelect($Hash);

// Все сделано return $Hash;

}

function GetResult($r) { return $this->TableGetResult($r); }

//Примечание: мы используем две функции, из которых GetResult()

//просто является синонимом для TableGetResult(), чтобы позволить

//производному классу вызывать функции MysqlTable, даже если они

//переопределены в нем. К сожалению, в PHP это единственный метод

//добиться цели.

//Аналог mysql_num_rows()

function GetNumRows($r) { return mysql_num_rows($r); }

470

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

//Аналог mysql_data_seek(). После вызова этой функции указатель на

//дескриптор $r "перескочит" на найденную запись номер $to, после

//чего GetResult() ее и возвратит.

function DataSeek($r,$to) { return mysql_data_seek($r,$to); }

//Создает или загружает таблицу по имени $Name.

//$Fields — список полей базы. Именно по ним в дальнейшем можно

//будет вести поиск и строить индекс. Кроме того, в запись можно будет

//добавлять ЛЮБЫЕ другие переменные, но они будут сериализованы, а

//потом восстановлены. Формат списка: массив с ключами — именами

//переменных и значениями — их типами. Если $Fields — не массив, то

//считается, что таблица открывается такой, какой она есть. В противном

//случае производится проверка: не добавились или не удалились ли какие-

//то поля или индексы и, если это так, то выполняется соответствующая

//модификация таблицы (кстати, это процесс довольно длительный).

//ВНИМАНИЕ: если в таблице было какое-то поле, которое сериализуется, то

//в будущем при добавлении этого поля к $Fields оно НЕ будет

//автоматически переведено в ранг несущих, т. е. попросту

//пропадет (и наоборот).

//РЕКОМЕНДАЦИЯ: перечисляйте в $Fields те поля, для которых вы ТОЧНО

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

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

//distinct.

//$Index — по каким полям нужно строить индекс. Индекс несколько

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

//(точнее, по тем полям, для которых используется индекс). Ключи — имена

//столбцов, значения — "размер" индекса (0, если по умолчанию, что чаще

//всего наиболее разумно)

function MysqlTable($Name,$Fields="",$Index="") { $this->TableName=$Name; $this->Error=""; if(is_array($Fields)) {

foreach($Fields as $k=>$v)

if(!eregi("not null",$v)) $Fields[$k]=$v." not null"; $Fields=array("id"=>"int auto_increment primary key") +$Fields+array(DataField=>"mediumblob");

}

Def0($Index,array());

// Считываем из таблицы поле с ее параметрами $this->Fields=array(DataField=>"mediumblob");