Главное меню

Написание кода класса

Итак, мы определили, каким должен быть вывод разрабатываемого кода, и какие функции он должна выполнять, и теперь нужно реализовать этот код. Позже речь пойдет о проектировании и управлении крупными проектами. А пока мы сосредоточим внимание на особенностях написания объектно-ориентированного РНР-кода.

Классу необходимо присвоить логическое имя. Поскольку он представляет страницу, назовем его Page. Для объявления класса Page следует ввести:

class Раде {

}

Разрабатываемому классу нужны атрибуты. Элементы, которые, возможно, придется изменять от страницы к странице, мы определим как атрибуты класса. Основное содержимое страницы, которое будет представлено комбинацией HTML-дескрипторов и текста, назовем $content. Это содержимое можно объявить посредством следующей строки кода внутри определения класса:

public $content;

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

public $title = "ВОВАН Convulsing Pty Ltd";

Большинство коммерческих веб-страниц включают в себя метадескрипторы, помогающие поисковым механизмам выполнять их индексацию. Чтобы они были полезны, метадескрипторы, по-видимому, должны изменяться от страницы к странице. В этом случае мы также определяем значение по умолчанию:

public $keywords = "ВОВАН Convulsing, Реальный сайт, поисковые механизмы – мои лучшие друзья";

Написание кода класса

Навигационные кнопки, показанные на исходной странице, изображенной на рис. 5.2, скорее всего, должны оставаться неизменными на всех страницах, чтобы посетители не путались; однако, чтобы их можно было легко изменить, их также лучше сделать атрибутами. Поскольку количество кнопок может меняться, мы объявляем массив и сохраняем в нем как текст кнопки, так и URL (Uniform Resource Locator – унифицированный указатель информационного ресурса), на который она должна указывать.

public $buttons = array( "Главная" => "home.php",

"Контакты" => "contact.php",

"Услуги" => "services.php",

"Карта сайта" => "map.php"

);

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

public function __set($name, $value) {

$this->$name = $value;

}

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

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

public function Display() {

echo "<html>\n<head>\n";

$this -> DisplayTitle();

$this -> DisplayKeywords();

$this -> DisplayStyles();

echo "</head>\n<body>\n";

$this -> DisplayHeader();

$this -> DisplayMenu($this->buttons);

echo $this->content;

$this -> DisplayFooter();

echo "</body>\n</html>\n";

}

Данная функция включает в себя несколько простых операторов echo для отображения HTML-текста, но в основном состоит из вызовов других функций класса. Как нетрудно догадаться по их именам, эти функции отображают различные части страницы.

Совсем не обязательно организовывать функции подобным образом. Все эти отдельные функции можно было бы объединить в одну большую функцию. Мы же ввели такое разделение по ряду причин.

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

Используя наследование, мы можем выполнять переопределение операций. Можно заместить одну крупную функцию Display(), однако маловероятно, чтобы мы захотели изменить способ отображения всей страницы. Гораздо рациональнее разбить действия по отображению на несколько самостоятельных задач и иметь возможность выполнять переопределение только тех частей, которые требуется изменить.

Функция Display (отобразить) вызывает функции DisplayTitle() (отобразить заголовок), DisplayKeywords() (отобразить ключевые слова), DisplayStyles() (отобразить стили), DisplayHeader() (отобразить верхний колонтитул), DisplayMenu() (отобразить меню) и DisplayFooter() (отобразить нижний колонтитул). Значит, необходимо определить эти операции. Операции или функции можно записывать в этом логическом порядке и вызывать операцию или функцию еще до того, как в программе встретится фактический код этой операции или функции. Во многих других языках код функции или операции должен быть записан до ее вызова. Большинство используемых в данном случае операций весьма просты и необходимы для отображения некоторого HTML-текста и, возможно, содержимого атрибутов.

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

Написание кода класса

При просмотре этого листинга обратите внимание, что функции DisplayStyles(), DisplayHeader() и DisplayFooter() должны отображать большой блок статического HTML-кода без какой-либо обработки с помощью РНР. Поэтому внутри функций мы просто указали завершающий РНР-дескриптор (?>), ввели HTML-код, а затем с помощью открывающего РНР-дескриптора (<?php) снова перешли к РНР.

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

Операция IsURLCurrentPage() определяет, указывает ли связанный с кнопкой URL на текущую страницу. Для этого служит множество технологий. Мы воспользовались строковой функцией strpos(), чтобы определить, содержится ли данный URL в одной из переменных, установленных сервером.

Оператор strpos($_SERVER['PHP_SELF'], $url) возвращает число, если строка, хранящаяся в переменной $url, присутствует внутри суперглобальной переменной $_SERVER['PHP_SELF'], либо значение false, если это не так.

Чтобы можно было пользоваться классом Page, файл page.inc потребуется включить в сценарий и вызвать Display().

Код, показанный в листинге 6.2, создает домашнюю страницу сайта компании ВОВАН Convulsing и обеспечивает вывод, который очень похож на сгенерированный ранее (см. рис. 5.2). Этот код выполняет перечисленные ниже действия.

1. Использует оператор require для включения содержимого файла page.inc, который содержит определение класса Page.

2. Создает экземпляр класса Page. Этому экземпляру назначается имя $homepage.

3. Создает контент – некоторый текст и HTML-дескрипторы, которые должны быть на странице. (При этом неявно вызывается __set.)

4. Вызывает операцию Display() внутри объекта $homepage, которая обеспечивает отображение страницы в окне браузера посетителя.

Написание кода класса

Как видно из листинга 6.2, для генерации новых страниц с использованием класса Page требуется выполнить совсем незначительный объем работы. Такое использование класса означает, что все страницы будут очень похожи друг от друга.

Если нужно, чтобы в некоторых разделах сайта использовался вариант стандартной страницы, можно просто скопировать page.inc в новый файл page2.inc и внести в него некоторые изменения. Это значит, что при каждом обновлении или исправлении в файле page.inc нельзя забывать внести эти же изменения и в файл page2.inc.

Более рациональный подход предусматривает использование механизма наследования для создания нового класса, который наследует большую часть своих функциональных возможностей от класса Page, но переопределяет те части, которые должны отличаться. Нам нужно, чтобы страница услуг на сайте ВОВАН Convulsing содержала вторую навигационную панель.

Сценарий, показанный в листинге 6.3, решает эту задачу путем создания нового класса ServicesPage, унаследованного от Page. В этом классе мы определяем новый массив $row2buttons, который содержит кнопки и ссылки, необходимые во второй строке. Поскольку мы хотим, чтобы этот класс в основном сохранил поведение родительского класса, мы переопределяем только ту часть, которая должна изменяться, а именно – операцию Display().

Написание кода класса

Функция Display() с переопределением очень похожа на функцию в родительском классе, но содержит одну дополнительную строку:

$this -> DisplayMenu($this->row2buttons);

которая второй раз вызывает операцию DisplayMenu() и создает вторую панель меню.

За пределами определения класса мы создаем экземпляр класса ServicesPage, устанавливаем значения, которые должны отличаться от значений по умолчанию, и вызываем операцию Display().

На рис. 6.2 показан новый вариант стандартной страницы. При этом нам пришлось создать код только для отличающихся частей страниц.

Написание кода класса

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

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

Дополнительные объектно-ориентированные возможности в РНР

Далее рассмотрим дополнительные объектно-ориентированные возможности РНР.

Использование констант класса

В РНР возможно создание констант класса. Константа класса может использоваться без необходимости создания экземпляра класса, как показано в следующем примере:

<?php

class Math {

const pi = 3.14159;

}

echo "Math::pi = ".Math::pi."\n";

?>

Доступ к константам класса осуществляется с помощью операции :: и указания имени класса, которому константа принадлежит.

Реализация статических методов

В РНР имеется ключевое слово static. Оно применяется к методам, позволяя вызывать их без необходимости создания экземпляра класса. Идея статических методов подобна идее констант класса. Например, вернемся еще раз к классу Math, который упоминался в предыдущем примере. Вы можете добавить к нему статический метод squared() и вызывать его, не создавая экземпляр класса:

class Math {

static function squared($input) {

return $input*$input;

}

}

echo Math::squared(8);

Обратите внимание, что внутри статического метода нельзя использовать ключевое слово this, поскольку может не существовать ни одного экземпляра для ссылки.

Проверка типа объекта и указание типов

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

• ($b instanceof В) в результате дает true;

• ($b instanceof А) в результате дает true;

• ($b instanceof Displayable) в результате дает false.

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

Кроме того, в РНР5 возможно указание типов. Обычно при передаче параметра в РНР-функцию тип этого параметра не указывается. С помощью указания типов можно задать тип класса, который должен передаваться, и если тип фактического параметра не совпадает с ним, генерируется ошибка. Проверка типов эквивалентна instanceof.

Например, предположим, что имеется следующая функция:

function check_hint(B $someclass)

{

//...

}

В этой функции указано, что параметр $someclass должен быть экземпляром класса В. Если передать в функцию экземпляр класса А, т.е.:

check_hint($а);

будет получена следующая фатальная ошибка:

Fatal error: Argument 1 must be an instance of В

Фатальная ошибка: Аргумент 1 должен быть экземпляром В

Следует отметить, что если указать для параметра тип класса А и затем передать в функцию экземпляр класса В, ошибка не возникнет, поскольку В унаследован от А.

Позднее статическое связывание

Позднее статическое связывание, которое появилось в версии РНР 5.3, позволяет производить ссылки на вызванный класс в контексте статического наследования; родительские классы могут использовать статические методы, переопределенные дочерними классами. Следующий простой пример из руководства по РНР демонстрирует действие позднего статического связывания:

<?php

class А {

public static function who() {

echo __CLASS__;

}

public static function test() {

static::who (); // Здесь выполняется позднее статическое связывание

}

}

class В extends А {

public static function who() {

echo __CLASS__;

}

}

B::test();

?>

Этот пример дает следующий вывод:

Разрешение ссылок на классы, вызываемые во время выполнения, независимо от того, переопределены они или нет, предлагает дополнительные возможности.

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

В РНР можно также использовать ключевое слово clone, которое позволяет копировать существующие объекты.

Например, оператор:

$с = clone $b;

создает копию объекта $b того же самого класса с теми же самыми значениями атрибутов.

Вы можете изменить данное поведение. Если вам необходимо нестандартное поведение clone, создайте в базовом классе метод с именем __clone(). Этот метод похож на конструктор или деструктор и не вызывается напрямую. Он вызывается, когда используется ключевое слово clone, как показано выше. Внутри метода __clone() можно определить требуемые действия по копированию.

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

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

Иногда необходимо выполнить какие-то другие действия, например, обновить базу данных на основе информации, хранящейся в экземпляре класса.

Использование абстрактных классов

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

abstract operationX ($param1, $param2) ;

Любой класс, содержащий абстрактные методы, должен быть абстрактным:

abstract class А {

abstract function operationX($param1, $param2);

}

Основное применение абстрактных методов и классов связано с построением сложной иерархии классов, в которой каждый подкласс должен содержать и переопределять определенные методы; этого же можно достигнуть и посредством интерфейсов.

Перегрузка методов с помощью метода __call()

Ранее мы сталкивались с несколькими методами класса специального назначения, имена которых начинались с двух подчеркиваний (__): __get(), __set(), __construct() и __destruct(). Еще одним примером такого метода может служить метод __call(), который используется в РНР для перегрузки методов.

Перегрузка методов характерна для многих объектно-ориентированных языков, однако она не настоль полезна в РНР, так как в основном здесь применяются гибкие типы и простые в реализации необязательные функциональные параметры.

Для использования перегрузки функций потребуется реализовать метод __call(), как показано в примере ниже:

public function __call($method, $p) {

if ($method == "display") {

if (is_object($p[0])) {

$this->displayObject($p[0]);

} else if (is_array($p[0])) {

$this->displayArray($p[0]);

} else {

$this->displayScalar($p[0]);

}

}

}

Метод __call() принимает два параметра. Первый параметр содержит имя вызываемого метода, а второй – массив параметров, передаваемых этому методу. В теле __call() принимается решение о том, какой конкретно метод должен быть вызван. В данное случае, если методу display() передан объект, вызывается метод displayObject(), если передан массив – вызывается метод displayArray(), а во всех остальных случаях – метод displayScalar(). Для вызова показанного выше кода сначала потребуется создать экземпляр класс, содержащего метод __саll() (пусть он называется overload), а затем обратиться к методу display():

$ov = new overload;

$ov->display(array(1, 2, 3) ) ;

$ov->display('кот') ;

Первый вызов display() приведет к выполнению displayArray(), а второй вызов – к выполнению displayScalar().

Для того чтобы этот код работал, вам не нужна ни одна display().

Использование autoload()

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

Основное назначение __autoload() состоит во включении или затребовании всех файлов, необходимых для создания экземпляров требуемого класса. Рассмотрим следующий пример:

function __autoload($name) {

include_once $name.".php";

}

Эта реализация предпринимает попытку включить файл с именем, совпадающим с именем класса.

Реализация итераторов и итерации

Исключительно полезная характеристика объектно-ориентированного механизма в РНР связана с возможностью использованйя цикла fоreach() для перебора атрибутов объекта подобно тому, как это делается в отношении элементов массива. Ниже показан пример:

class myClass {

public $а="5";

public $b="7";

public $c="9";

}

$x = new myClass;

foreach ($x as $attribute) {

echo $attribute."<br />";

}

(На момент написания этого материала официальное руководство по РНР советовало реализовать пустой интерфейс Traversable, чтобы интерфейс foreach нормально функционировал, однако если поступить подобным образом, возникает фатальная ошибка. А без Traversable все работает нормально.)

Если требуется получить более сложное поведение, необходимо создать итератор (iterator). Для этого реализуйте в классе, в котором нужно выполнить перебор, интерфейс IteratorAggregate, и напишите метод getIterator, возвращающий экземпляр класса итератора. Интерфейс Iterator содержит набор методов, которые также должны быть реализованы. Пример реализации класса и итератора показан в листинге 6.4.

Написание кода класса

Класс ObjectIterator имеет набор функций, требуемый интерфейсом Iterator.

• Конструктор не обязателен, однако, очевидно, он представляет собой хорошее место для установки количества итерируемых элементов и ссылки на текущий элемент.

• Функция rewind() должна устанавливать внутренний указатель данных на начало данных.

• Функция valid() должна сообщать, имеются ли еще данные в текущей позиции указателя.

• Функция key() должна возвращать значение указателя на данные.

• Функция value() должна возвращать значение, хранящееся по указателю данных.

• Функция next() должна перемещать указатель внутри данных.

Причина использования такого класса итератора состоит в том, что интерфейс к данным не изменяется, даже если изменяется представление данных. В рассмотренном примере класс IteratorAggregate представляет собой простой массив. Даже если вы решите изменить его, скажем, на хеш-таблицу или односвязный список, вы все равно сможете пользоваться стандартным интерфейсом Iterator для его перебора, несмотря на то, что код реализации интерфейса Iterator изменится.

Преобразование классов в строки

Если реализовать в классе функцию __toString(), она будет вызываться при попытке вывода класса на печать, как показано ниже:

$р = new Printable;

echo $р;

Все, что возвращает функция __toString(), будет выводиться на экран оператором echo. Эту функцию можно реализовать, например, следующим образом:

class Printable {

var $testone;

var $testtwo;

public function __toString() {

return(var_export ($this, TRUE));

}

}

(Функция var_export() выводит значения всех атрибутов класса.)

Использование Reflection API

К объектно-ориентированным возможностям РНР относится и API-интерфейс рефлексии Reflection API. Рефлексия – это способность опрашивать существующие классы и объекты на предмет информации об их структурах и содержимом. Данная возможность исключительно полезна при взаимодействии с неизвестными или недокументированными классами, которые представлены в закодированных РНР-сценариях.

Этот API-интерфейс чрезвычайно сложен, но мы все же рассмотрим простой пример, который позволит ухватить основную идею. В примере используется ранее определенный в этой главе класс Page. Получить всю информацию о классе Page с помощью Reflection API можно так, как показано в листинге 6.5.

Написание кода класса

Здесь для вывода на печать информации используется метод __toString() класса Reflection. Обратите внимание на дескрипторы <рге>, которые не имеют отношения к информации, выдаваемой методом __toString().

Один из экранов вывода, сгенерированного приведенным выше кодом, показан на рис. 6.3.

Написание кода класса