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

web - tec / PHP 5 для начинающи

.pdf
Скачиваний:
72
Добавлен:
12.06.2015
Размер:
26.79 Mб
Скачать

532 Глава 13

throw new Exception('Изменять значение поля ID нельзя!');

}

function phonenumbers($index)

{

if(!isset($this->_phonenumbers[$index])) {

throw new Exception('Указан неправильный номер телефона!'); } else {

return $this->_phonenumbers[$index];

}

}

function getNumberOfPhoneNumbers()

{

return sizeof($this->_phonenumbers);

}

function addPhoneNumber(PhoneNumber $phone)

{

$this->_phonenumbers[] = $phone;

}

function addresses($index)

{

if(!isset($this->_addresses[$index]))

{

throw new Exception('Указан неправильный адрес!'); } else {

return $this->_addresses[$index];

}

}

function getNumberOfAddresses()

{

return sizeof($this->_addresses);

}

function addAddress(Address $address)

{

$this->_addresses[] = $address;

}

function emails($index)

{

if(!isset($this->_emails[$index]))

{

throw new Exception('Указан неправильный email-адрес!'); } else

{

return $this->_emails[$index];

}

}

function getNumberOfEmails()

{

return sizeof($this->_emails);

}

function addEmail(Email $email)

{

$this->_emails[] = $email;

}

function _updateEntity()

{

Работа с UML и классами 533

$sql = "UPDATE entities SET ";

//Сформируем SQL-оператор, создав

//массив SET-операторов, а затем объединив элементы этого массива, //разделяя их запятыми.

$setStatements = array(); foreach($this->_changedProperties as $prop) {

$setStatements[] = "$prop = '{$this->_properties[$prop]}'";

}

//формируем строку

$sql .= join(', ', $setStatements);

//присоединяем предложение WHERE

$sql .= " WHERE entityid = $this->id"; print $sql;

$hRes = mysql_query($sql, $this->hDB); $intAffected = mysql_affected_rows($hRes);

}

function _createEntity()

{

$data = array();

$data['name1'] = "'" . mysql_escape_string($this->name1) . "'"; $data['name2'] = "'" . mysql_escape_string($this->name2) . "'"; $data['type'] = "'" . mysql_escape_string($this->type) . "'";

$sql = "INSERT INTO entities (" . join(',' array_keys($data)). ") "; $sql .= " (" . join(', ', array_values($data)) . ")";

$res = mysql_query($sql, $this->hDB);

if (!$res) {

//Обратите внимание, невозможно перехватить исключение, сгенерированное //в деструкторе (деструктор вызывает эту функцию), поэтому

// придется использовать trigger_error для сообщения о проблеме. trigger_error("Во время сохранения объекта возникла ошибка!",

E_USER_WARNING);

}

}

function __destruct()

{

//Проверка наличия изменений. if(sizeof($this->_changedProperties)) {

if($this->id) {

//обновляется существующая запись; $this->_updateEntity();

} else {

//создается новая запись $this->_createEntity();

}

}

//Работа окончена, закрываем соединение с базой данных. mysql_close($this->_hDB);

}

}

?>

Чтобы создать MySQL+таблицу entities, следует ввести следующий SQL+оператор:

CREATE TABLE entities (

entityid INT NOT NULL AUTO_INCREMENT PRIMARY KEY, name1 varchar(100) NOT NULL,

534 Глава 13

name2 varchar(100) NOT NULL, type char(1) NOT NULL

);

Чтобы дать возможность классу Entity работать с телефонными номерами, поч+ товыми и e+mail+адресами, в первоначальный класс User было внесено несколько из+ менений и дополнений. Теперь три частные переменные экземпляра: $_emails, $_addresses и $_phonenumbers ++++++ инициализируются в конструкторе как пустые массивы, в которых затем будут храниться объекты Address, Email и PhoneNumber, связанные с текущим объектом Entity.

Добавлена частная функция инициализации с именем _init(), которая вызыва+ ется конструктором. Функция впоследствии вызывает четыре частные init+функции, которые обрабатывают заполнение объекта данными. Функция _initUser() выпол+ няет такой же поиск в базе данных и те же функции назначения данных, которые бы+ ли в конструкторе класса User. Добавляются также init+функции для трех типов контактной информации. Эти функции пока пусты, но будут содержать код для созда+ ния объектов Address, Email и PhoneNumber в упомянутых выше массивах.

Для каждого типа контактной информации добавляется три функции: getNumberOf[x], add[x] и соответственно emails, addresses или phonenumbers. Функция getNumberOf[x] сообщает количество существующих объектов в частных массивах, а add[x] позволяет добавлять новые объекты. Остальные функции не так очевидны, но в конечном итоге позволяют написать подобный код:

for($x = 0; $x < $obj->numberOfEmails; $x++ ) {

print "<a href=\"mailto:{$obj->emails($x)->email}\">" . $obj->emails($x)->email .

"</a> (" . $obj->emails($x)->type . ")<br>\n";

}

В сущности, они позволяют использовать удобный синтаксис для перебора семей+ ства типов контактной информации и получения доступа к отдельным элементам со+ ответствующих массивов. Несмотря на то что эти функции обозначены в UML+ диаграмме как свойства, реализация их в виде функций позволяет, используя класс Entity, писать более четкий и простой для понимания код.

Кроме того, стоит отметить поля базы данных name1 и name2, а также новую функцию getName(). Для организаций используется только одно поле с именем, то+ гда как для частных лиц ++++++ два (имя и фамилия). Чтобы приспособиться к этому, класс Entity содержит оба значения, но по умолчанию свойство name возвращает только значение поля name1 (в котором хранится название организации). В классе Individual это свойство будет возвращать имя и фамилию.

Каждый объект должен отвечать за свои внутренние операции, поэтому массив $_changedProperties при добавлении нового типа контактной информации не об+ новляется. Вместо этого упомянутые классы будут сами следить за вставкой своих данных в базу и их обновлением.

Рассмотрим новый усовершенствованный деструктор. Объект теперь имеет возмож+ ность сохранять любые изменения своих данных, а также создавать новые записи. В за+ висимости от того, существует ли запись (элемент) или создается новая, вызывается ли+ бо функция _saveEntity(), либо функция _updateEntity(). Новая запись создается, если во время уничтожения объекта отсутствует значение свойства id. Можно предпо+ ложить, что если свойство id равно NULL, то объект еще не имеет записи в таблице entities (потому что поле entityid определено в базе данных как NOT NULL), и по+ этому выполняется INSERT+оператор, создающий новую запись. Следует отметить, что функция setID() генерирует ошибку при любой попытке изменить значение свойства id.

Работа с UML и классами 535

Сборка классов

В главе 12 было сказано, что очень полезно защищать данные в частных перемен+ ных экземпляра и использовать функции для доступа ко всем свойствам класса, ис+ пользование методов __get и __set весьма упрощает этот процесс. Показанные

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

Поскольку читатели уже знают о наследовании и о том, какие преимущества оно дает

вPHP, следует убрать всю специализированную функциональность из класса Entity и создать класс, который позволил бы повторно использовать этот код в других клас+ сах. Первоначально этот класс назывался PropertyObject.

Класс PropertyObject

Исходные классы PropertyObject и Entity не имели унифицированного сред+ ства для проверки корректности данных, поэтому требуется интерфейс для всех дан+ ных, которые можно проверить. Рассмотрим код такого интерфейса:

<?php

interface Validator {

abstract function validate();

}

?>

Назовите этот файл interface.Validator.php, он будет использоваться в но+ вом усовершенствованном классе PropertyObject. Приведенный ниже код показы+ вает, как интегрировать интерфейс Validator в класс PropertyObject. Введите следующий код в файл class.PropertyObject.php.

<?php require_once('interface.Validator.php');

abstract class PropertyObject implements Validator {

protected $propertyTable = array();

//Пары имя/значение,

 

//которые преобразуют свойства

 

//в имена полей базы данных

protected $changedProperties = array(); //Перечень свойств, которые

 

//были изменены

protected $data;

//Реальные данные из базы

protected $errors = array();

//Возникшие ошибки

 

//проверки данных

public function __construct($arData) {

 

$this->data = $arData;

 

}

 

function __get($propertyName) {

if(!array_key_exists($propertyName, $this->propertyTable))

throw new Exception("Неправильное свойство \"$propertyName\"!");

if(method_exists($this, 'get' . $propertyName)) {

536 Глава 13

return call_user_func(array($this, 'get' . $propertyName));

}else {

return $this->data[$this->propertyTable[$propertyName]];

}

}

function __set($propertyName, $value) {

if(!array_key_exists($propertyName, $this->propertyTable))

throw new Exception("Неправильное свойство \"$propertyName\"!");

if(method_exists($this, 'set' . $propertyName)) { return call_user_func(

array($this, 'set' . $propertyName), $value

);

}else {

//Если значение свойства действительно было изменено, //но еще не попало в массив changedProperties,

//то добавить его в этот массив. if($this->propertyTable[$propertyName] != $value &&

!in_array($propertyName, $this->changedProperties)) { $this->changedProperties[] = $propertyName;

}

//Устанавливаем новое значение свойства $this->data[$this->propertyTable[$propertyName]] = $value;

}

}

function validate() {}

}

?>

Рассмотрим работу этого кода подробнее. Создается четыре защищенные пере+ менные экземпляра. Защищенные переменные экземпляра видимы только внутри подклассов данного класса; они не видимы для кода, который использует эти объекты.

Переменная $propertyTable содержит таблицу преобразования имен свойств

вимена полей базы данных. Часто в именах полей используются префиксы, указы+ вающие на тип хранимых в поле данных. Например, поле entities.sname1 может быть полем таблицы entities, имеющим строковый тип и содержащим имя. Однако имя sname1 не особенно удобно использовать в качестве имени свойства объекта, по+ этому необходимо обеспечить механизм преобразования имен полей базы данных

вудобные имена свойств.

Переменная $changedProperties служит той же цели, что и в предыдущей реа+ лизации класса. Она представляет собой массив, в котором хранится список имен из+ мененных свойств.

$data ++++++ ассоциативный массив имен и значений полей базы данных. Этот мас+ сив передается конструктору со структурой данных, поступающей непосредственно от функции pg_fetch_assoc() (или mysql_fetch_assoc() или любой другой

[x]_fetch_assoc()+функции). Как будет показано далее, это значительно упрощает создание удобных объектов на основе запросов к базе данных.

Работа с UML и классами 537

В последней переменной $errors содержится массив имен полей и сообщений об ошибках на случай, если метод validate (требуемый интерфейсом Validate) по+ терпит неудачу.

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

Рассмотрим значительно упрощенный конструктор. Все, что делает конструк+ тор ++++++ принимает ассоциативный массив, который, скорее всего, заполняется данны+ ми в результате запроса к базе данных, и присваивает этот массив защищенной пере+ менной $data. В большинстве подклассов класса PropertyObject конструктор необходимо переопределить и заставить его выполнять нечто более интересное.

Еще одним важным изменением является внутреннее устройство функций __get() и __set(). Так как данные хранятся в переменной $data, требуется возможность связы+ вать имена свойств с соответствующими именами полей в базе данных. Эту задачу решают строки, содержащие код $this->data[$this->propertyTable[$propertyName]]. Выбор и присвоение значений в массиве $data с использованием имен полей базы данных, а не имен свойств, позволяет легко реализовать способность базы данных ав+ томатически создавать и поддерживать перманентные объекты, которая уже была реализована в классе Entity.

Если назначение переменных $data и $propertyTable не совсем ясно, огорчаться не стоит, поскольку все будет понятно после изучения соответствующего примера.

Классы, представляющие типы контактной информации

Теперь можно применить класс PropertyObject. Ниже представлен код классов

Address, EmailAddress и PhoneNumber.

Вкоде имеется ссылка на класс DataManager, который должен использоваться

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

Введите следующий код (класс Address) в файл с именем class.Address.php:

<?php require_once('class.PropertyObject.php');

class Address extends PropertyObject {

function __construct($addressid) {

$arData = DataManager::getAddressData($addressid);

parent::__construct($arData);

$this->propertyTable['addressid'] = 'addressid'; $this->propertyTable['id'] = 'addressid'; $this->propertyTable['entityid'] = 'entityid'; $this->propertyTable['address1'] = 'saddress1';

538 Глава 13

$this->propertyTable['address2'] = 'saddress2'; $this->propertyTable['city'] = 'scity'; $this->propertyTable['state'] = 'cstate'; $this->propertyTable['zipcode'] = 'spostalcode'; $this->propertyTable['type'] = 'stype';

}

function validate() {

if(strlen($this->state) != 2) {

$this->errors['state'] = 'Пожалуйста, выберите существующий штат.';

}

if(strlen($this->zipcode) < 5 ||

strlen($this->zipcode) > 10) {

$this->errors['zipcode'] = 'Пожалуйста, введите почтовый индекс из 5 или 9 цифр';

}

if(!$this->address1) {

$this->errors['address1'] = 'Поле адрес 1 обязательно для заполнения.';

}

if(!$this->city) {

$this->errors['city'] = 'Поле город обязательно для заполнения.';

}

if(sizeof($this->errors)) { return false;

} else { return true;

}

}

function __toString() {

return $this->address1 . ', ' . $this->address2 . ', ' . $this->city . ', ' .

$this->state . ' ' . $this->zipcode;

}

}

?>

Поскольку основную работу берет на себя класс PropertyObject, в классе Address необходимо реализовать только два метода (функция __toString() реализована про+ сто ради интереса). В конструкторе впервые используется массив $propertyTable. Перечень необходимых свойств класса был определен в UML+диаграмме при первона+ чальном проектировании приложения (в начале главы). Учитывая свойства объекта, можно также определить структуру таблицы базы данных. Как правило, для каждого по+ ля требуется одно поле, и поскольку данный класс должен быть связан с классом Entity, требуется также хранить ссылку на родительский объект Entity. Предполо+ жим, для создания таблицы Address используется следующий SQL+оператор:

CREATE TABLE entityaddress (

addressid int NOT NULL AUTO_INCREMENT PRIMARY KEY, entityid int,

saddress1 varchar(255), saddress2 varchar(255), scity varchar(255), cstate char(2), spostalcode varchar(10), stype varchar(50),

Работа с UML и классами 539

CONSTRAINT fk_entityaddress_entityid

FOREIGN KEY (entityid) REFERENCES entity(entityid)

);

Соответствующее именование полей базы данных с использованием односим+ вольного префикса для указания типа данных позволяет узнать, какой тип данных хранится в том или ином поле. Соглашения по именованию баз данных так же важны, как и соглашения по именованию для кода.

Массив propertyTable, создаваемый в классе Address, связывает удобные имена свойств (например, city, state и zipcode) с менее удобными именами полей в базе данных (scity, cstate и spostalcode). Следует отметить, что к одному име+ ни поля в массиве propertyTable можно привязать несколько имен свойств. Это по+ зволяет обращаться к первичному ключу либо как $objAddress->addressed, либо как $objAddress->id.

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

Ниже приведен код для класса Email, который очень похож на предыдущий класс. Введите следующий код в файл class.EmailAddress.php.

<?php require_once('class.PropertyObject.php');

class EmailAddress extends PropertyObject {

function __construct($emailid) {

$arData = DataManager::getEmailData($emailid);

parent::__construct($arData);

$this->propertyTable['emailid'] = 'emailid'; $this->propertyTable['id'] = 'emailid'; $this->propertyTable['entityid'] = 'entityid'; $this->propertyTable['email'] = 'semail'; $this->propertyTable['type'] = 'stype';

}

function validate() {

if(!$this->email) {

$this->errors['email'] = 'Поле email обязательно для заполнения.';

}

if(sizeof($this->errors)) { return false;

} else { return true;

}

}

function __toString() { return $this->email;

}

}

?>

540 Глава 13

Здесь добавляется очень немного кода, только выборка данных из базы и создание массива propertyTable. Все остальное ++++++ проверка корректности данных. UML+ диаграмма в данном случае также подсказывает решения о свойствах класса Email и структуре соответствующей таблицы в базе данных.

Таблица entityemail выглядит следующим образом:

CREATE TABLE entityemail (

emailid int NOT NULL AUTO_INCREMENT PRIMARY KEY, entityid int,

semail varchar(255), stype varchar(50),

CONSTRAINT fk_entityemail_entityid

FOREIGN KEY (entityid) REFERENCES entity(entityid)

);

Класс PhoneNumber работает почти так же, как классы Address и Email. Ниже приведен код, который следует ввести в файл class.PhoneNumber.php:

<?php require_once('class.PropertyObject.php');

class PhoneNumber extends PropertyObject {

function __construct($phoneid) {

$arData = DataManager::getPhoneNumberData($phoneid);

parent::__construct($arData);

$this->propertyTable['phoneid'] = 'phoneid'; $this->propertyTable['id'] = 'phoneid'; $this->propertyTable['entityid'] = 'entityid'; $this->propertyTable['number'] = 'snumber'; $this->propertyTable['extension'] = 'sextension'; $this->propertyTable['type'] = 'stype';

}

function validate() {

if(!$this->number) {

$this->errors['number'] = 'Необходимо ввести номер телефона.';

}

if(sizeof($this->errors)) { return false;

}else { return true;

}

}

function __toString() { return $this->number .

($this->extension ? ' x' . $this->extension : '');

}

}

?>

SQL+оператор для создания таблицы entityphone выглядит так:

CREATE TABLE entityphone (

phoneid int NOT NULL AUTO_INCREMENT PRIMARY KEY, entityid int,

snumber varchar(20), sextension varchar(20),

Работа с UML и классами 541

stype varchar(50),

CONSTRAINT fk_entityemail_entityid

FOREIGN KEY (entityid) REFERENCES entity(entityid)

);

Класс DataManager используется для наполнения собственных объектов данны+ ми, создания массива propertyTable и определения некоторых правил проверки данных. Функция __toString реализована для того, чтобы упростить вывод полных номеров телефонов.

Класс DataManager

Рассмотрим класс DataManager. В нем, как и в остальных примерах кода для работы с базами данных, используется MySQL, хотя такой класс вполне может работать с любой другой СУРБД, например, PostGreSQL или Oracle. Основная задача класса DataManager заключается в том, чтобы собрать весь код для доступа к данным в одном месте, упро+ стив таким образом возможное изменение типа базы данных или параметров подклю+ чения к ней. Все методы класса объявлены как статические, потому что класс не использует переменные экземпляра. Следует отметить использование статической переменной в функции getConnection(). Это сделано для того, чтобы гарантировать, что во время запроса одной страницы будет открыто только одно подключение к базе данных. С ус+ тановкой подключения к базе данных связаны большие издержки, поэтому устранение ненужных подключений способствует повышению производительности. Создайте файл class.DataManager.php и введите в него следующий код:

<?php

require_once('class.Entity.php'); //это понадобится позднее require_once('class.Individual.php'); require_once('class.Organization.php');

class DataManager {

private static function _getConnection() { static $hDB;

if(isset($hDB)) { return $hDB;

}

$hDB = mysql_connect('localhost', 'phpuser', 'phppass') or die("Не удалось подключиться к базе данных!");

$Link = mysql_select_db('sample_db') or die('Не удалось выбрать базу данных!'); return $hDB;

}

public static function getAddressData($addressID) {

$sql = "SELECT * FROM entityaddress WHERE addressid = $addressID"; $res = mysql_query($sql, DataManager::_getConnection());

if(! ($res && mysql_num_rows($res))) {

die("Не удалось получить данные для адреса $addressID");

}

return mysql_fetch_assoc($res);

}

public static function getEmailData($emailID) {

$sql = "SELECT * FROM entityemail WHERE emailid = $emailID";

Соседние файлы в папке web - tec