Самоучитель PHP 4 - Котеров Д. В.
.pdf458 |
Часть 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");
Глава 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...)
