web - tec / PHP 5 для начинающи
.pdfВведение в объектно*ориентированное программирование 493
Как показывает предыдущий пример, значение свойства name может быть любым, включая объект, массив целых чисел, дескриптор файла и др., однако во время уста+ новки свойства name невозможно выполнить какую+либо проверку данных или обно+ вить другие значения.
Чтобы обойти эту проблему, рекомендуется реализовывать свойства в форме функций get[имя_Свойства] и set[имя_Свойства], как показано ниже в разделе ‘‘Практика’’.
Практика Доступ к свойствам посредством методов get и set
Внесите в файл class.Demo.php выделенные изменения:
<?php
class Demo {
private $_name;
public function sayHello() {
print "Hello {$this->getName()}!";
}
public function getName() { return $this->_name;
}
public function setName($name) {
if(!is_string($name) || strlen($name) == 0) { throw new Exception("Недопустимое имя");
}
$this->_name = $name;
}
}
?>
Отредактируйте файл testdemo.php как показано ниже:
<?php require_once('class.Demo.php'); $objDemo = new Demo();
$objDemo->setName('Steve'); $objDemo->sayHello();
$objDemo->setName(37); //генерирует ошибку
?>
Как это работает
Уровень доступа к свойству name изменился с public на private и в начале име+ ни переменной появился символ подчеркивания. Рекомендуется использовать символ подчеркивания для обозначения частных переменных и функций экземпляра. Вместе с тем это лишь соглашение и PHP этого не требует. Ключевое слово private не по+
494 Глава 12
зволяет коду, находящемуся за пределами объекта, модифицировать данное значение. Частные внутренние переменные экземпляра недоступны снаружи класса. Так как по+ лучить непосредственный доступ к этим переменным невозможно, для работы с дан+ ной информацией приходится использовать методы getName() и setName(), кото+ рые гарантируют, что класс сможет проверить значение до того, как оно будет присвоено свойству. В данном примере генерируется исключительная ситуация, если для свойства name вводится недопустимое значение. Кроме того, в код добавлен спе+ цификатор доступа public для функций.
Для работы со свойствами рекомендуется всегда использовать функции get и set. Это позволит в будущем значительно упростить реализацию требований бизнес+ логики и проверки данных.
Использование функций __get и __set
Настоятельно рекомендуется придерживаться совета об использовании методов get и set для всех свойств. Вместе с тем для крупных объектов с десятками свойств это может привести к необходимости создавать большое количество кода. Это особенно нежелательно, если для большинства свойств нет непосредственных требований биз+ нес+логики. В таких ситуациях оказываются полезными методы __get и __set.
Если сценарий пытается получить доступ к свойству объекта, но открытой перемен+ ной с таким именем нет, то PHP проверяет, определена ли для данного объекта функция __get. Если такая функция определена, PHP автоматически вызывает ее с целью опре+ делить значение данного свойства. Аналогично, когда значение присваивается несуще+ ствующему свойству, PHP вызывает функцию __set, если она была определена.
Практика Использование методов __get и __set
Данный пример показывает, как использовать функции __get и __set для облег+ чения работы со свойствами. Создайте файл с именем class.PropertyObject.php и введите в него следующий код:
<?php
class PropertyObject {
private $_properties = array( 'name' => null, 'dateofbirth' => null
);
function __get($propertyName) {
if(!array_key_exists($propertyName, $this->_properties)) throw new Exception('Недопустимое значение свойства!');
if(method_exists($this, 'get' . $propertyName)) {
return call_user_func(array($this, 'get' . $propertyName));
}else {
return $this->_properties[$propertyName];
}
}
function __set($propertyName, $value) {
if(!array_key_exists($propertyName, $this->_properties)) throw new Exception('Недопустимое значение свойства!');
Введение в объектно*ориентированное программирование 495
if(method_exists($this, 'set' . $propertyName)) {
return call_user_func(
array($this, 'set' . $propertyName), $value
);
} else {
$this->_properties[$propertyName] = $value;
}
}
function setDateOfBirth($dob) {
if(strtotime($dob) == -1) {
throw new Exception("Недопустимое значение даты рождения!");
}
$this->_properties['dateofbirth'] = $dob;
}
function sayHello() {
//$this->_properties['name'] и $this->_properties['dateofbirth'] //доступны посредством метода __get
print "Привет! Меня зовут $this->name. Я родился $this->dateofbirth";
}
}
?>
Чтобы протестировать этот новый класс, создайте файл testpropertyobject.php со следующим кодом:
<?php require_once('class.PropertyObject.php');
$obj = new PropertyObject();
$obj->name = 'Борис'; //значение "Борис" присваивается переменной
//$_properties['name'] с помощью метода __set $obj->dateofbirth = '5 марта, 1977'; //Метод setDateOfBirth вызывается
функцией __set
$obj->sayHello();
$obj->dateofbirth = 'blue'; //Генерирует исключение
?>
Откройте страницу testpropertyobject.php в Web+браузере. На странице должно появиться сообщение: ‘‘Привет! Меня зовут Борис. Я родился 5 марта, 1977’’ и сообщение об ошибке, которое дает понять, что значение 'blue' не подходит для свойства dateofbirth.
Как это работает
Класс позволяет определить все допустимые свойства в массиве, элементы которо+ го инициализируются с некоторым приемлемым значением по умолчанию (в данном случае Null). PHP автоматически направляет функции __get любые попытки обра+ титься к свойствам, для которых не существует открытых переменных.
Вданном примере функция __get сначала проверяет, определено ли это свойство
вчастном массиве $_properties, и если нет, то генерируется исключительная ситуация. Если свойство определено, то проверяется наличие функции get[имя_Свойства]. Ес+
ли такая функция существует, то она вызывается посредством метода call_user_func. Он вызывает процедурную функцию, если первый из переданных ему параметров
Введение в объектно*ориентированное программирование 497
В целях обеспечения обратной совместимости PHP сначала ищет функцию с именем __construct, а если не находит ее, то как и раньше ищет функцию, которая имеет то же имя, что и класс.
Кроме того, конструктору можно передавать параметры, которые можно исполь+ зовать для инициализации значений свойств или для получения некоторой информа+ ции из базы данных. В следующем примере показано применение MySQL+функций для получения из базы данных информации о пользователях. В этом классе использу+ ются методы __get и __set, код которых взят из класса PropertyObject.
Практика Определение свойств объекта в конструкторе
Сохраните следующий код в файле class.User.php. Невыделенный код можно скопировать из файла class.PropertyObject.php. Для использования данного примера понадобится работающая MySQL+база данных. Чтобы использовать другую СУБД, можно просто заменить mysql_+функции функциями, соответствующими ис+ пользуемой платформе. Кроме того, следует удостовериться, что строка подключения соответствует требованиям используемой среды.
<?php
class User {
private $_properties; private $_hDB;
public function __construct($userID) { $this->_properties = array(); $this->_changedProperties = array(); $this->_properties['id'] = null; $this->_properties['username'] = null; $this->_properties['realname'] = null;
$this ->_hDB = mysql_connect('localhost', 'dbuser', 'mypassword');
if(! is_resource($this->_hDB)) {
throw new Exception("Невозможно подключиться к базе данных!");
}
$connected = mysql_select_db('mydatabase', $this ->_hDB);
if(! $connected) {
throw new Exception("Невозможно использовать базу данных 'mydatabase'!");
}
$sql = "select * from users where id = $userID"; $rs = mysql_query($sql, $this->_hDB);
if(! mysql_num_rows($rs)) {
throw new Exception("Пользователя с идентификатором $userID не существует!");
}
$row = mysql_fetch_assoc($rs);
$this->_properties['id'] = $row['id']; $this->_properties['username'] = $row['username']; $this->_properties['realname'] = $row['realname'];
}
498Глава 12
//Методы __get и __set для краткости опущены
//Запрещаем изменение идентификатора пользователя function setID($value) {
throw new Exception('Изменение идентификатора пользователя не допускается!');
}
function sayHello() {
print "Привет! Меня зовут {$this->realname}. Мой идентификатор: {$this->id}";
}
}
?>
Создайте файл testuser.php и введите в него следующий код:
<?php require_once('class.User.php');
$obj = new User(27); //Пользователь Боб Смит
$obj->sayHello();
//Выводит на экран строку "Привет! Меня зовут Боб Смит. Мой идентификатор: 27"
?>
Чтобы создать в MySQL+базе данных таблицу пользователей, можно ввести сле+ дующий SQL+оператор:
CREATE TABLE users (
id int NOT NULL AUTO_INCREMENT PRIMARY KEY, username varchar(50),
realname varchar(255) );
Используя приведенный ниже оператор INSERT, можно заполнить только что соз+ данную таблицу данными для тестирования класса:
mysql> INSERT INTO users (id, username, realname) VALUES( -> 27,
-> 'bsmith', -> 'Боб Смит');
Если передаваемый конструктору идентификатор пользователя отсутствует в базе данных и код создания объекта не был заключен в блок try... catch..., то на эк+ ране появится примерно следующее сообщение:
Fatal error: Uncaught exception 'exception' with message 'No user exists with id 12345!' in /path/to/class.User.php:28 Stack trace: # 0 /path/to/testuser.php(4): User->_ construct() # 1 {main} thrown in
/path/to/class.User.php on line 28
(Неисправимая ошибка: не перехваченное исключение 'exception'
с сообщением 'Пользователя с идентификатором 12345 не существует!' файл
/path/to/class.User.php:28 Трассировка стека: # 0
/path/to/testuser.php(4): User->_ construct() # 1 {main} генерируется в файле /path/to/class.User.php строка 28)
Введение в объектно*ориентированное программирование 499
Как это работает
Для хранения дескриптора ресурса подключения к базе данных создается частная переменная экземпляра $_hDB. Затем добавляется метод+конструктор __construct, которому передается параметр $userID. При создании новых объектов класса User конструктору передается идентификатор пользователя, данные которого необходимо выбрать из базы. Это значение представляет первичный ключ таблицы, в которой хранится информация о данном пользователе.
Конструктор открывает соединение с базой данных и сохраняет дескриптор ре+ сурса в переменной $this->_hDB. Затем необходимо вызвать функцию mysql_select_db для указания имени базы данных, с которой требуется установить связь. После этого вводится оператор SELECT для получения из базы данных всей информации о пользователе. Полученная информация записывается в массив $_properties, где она будет использоваться функциями __get, __set либо функ+ циями get[имяСвойства] или set[имяСвойства].
В реальных приложениях информация, необходимая для подключения к базе данных, обычно хранится в глобально подключаемом файле (см. главы 9, 10 и 11). В случае надобности это позволяет легко изменять параметры подключения к базе данных или имя самой базы данных. Такой файл обычно является единственным местом хранения этой информации и избавляет от необходимости заново просматривать множество разных файлов для обновления настроек приложения.
Использование конструктора с параметром позволяет инициализировать все зна+ чения свойств, извлекая их из базы данных. В данном случае извлекается информация о пользователе. Конечно, в реальных приложениях необходимо убедиться, что строка 4 файла testuser.php заключена в блок try... catch..., и перехватываются все ошибки, которые могут возникать при подключении к базе данных или выборке ин+ формации о пользователе.
Кроме того, следует отметить, что функция setID генерирует исключение при попытке изменить свойство, в котором хранится идентификатор пользователя. Так как данное свойство является первичным идентификатором пользователя, не реко+ мендуется разрешать его изменение со стороны пользователей класса.
В случае создания класса, который не требует какого+либо особого кода инициали+ зации, создавать конструктор нет необходимости. Как показано в предыдущем при+ мере очень простого класса Demo, PHP автоматически выполняет все нужные для соз+ дания объекта операции. Функцию конструктора рекомендуется создавать, только когда это действительно необходимо.
Уничтожение объекта
Создаваемая в сценарии объектная переменная удаляется из системной памяти, когда запрашиваемая страница полностью завершила свою работу, когда переменная выходит за пределы области видимости или когда она явно устанавливается в Null. В PHP5 можно перехватывать уничтожение объекта и предпринимать в этот момент какие+либо действия. Чтобы это сделать, необходимо создать функцию с именем __destruct, не имеющую параметров. Функция+деструктор, если она существует, вы+ зывается автоматически перед удалением объекта.
Это дает возможность в последний момент выполнять любые операции по очист+ ке, например, закрытие дескрипторов файлов или соединений с базами данных, ко+ торые могли быть открыты данным классом, или любые другие внутренние операции, которые требуется выполнить перед уничтожением объекта.
500 Глава 12
Практика Создание деструктора
Эффективность данной методики иллюстрируется на следующем примере, в кото+ ром используется код из созданного ранее класса User с добавленным деструктором. Модернизация кода позволяет объектам класса User автоматически сохранять свои собственные изменения в базе данных. Изменения свойств фиксируются. Когда объект уничтожается, сценарий проверяет, какое свойство было изменено, а затем обновля+ ет базу данных, используя новые значения свойств.
Внесите в файл class.User.php следующие изменения:
<?php
class User {
private $_properties;
private $_changedProperties; //содержит список
//измененных свойств
private $_hDB;
// методы __construct и __get для краткости опущены
function __set($propertyName, $value) {
if(!array_key_exists($propertyName, $this->_properties)) throw new Exception('Недопустимое значение свойства!');
if(method_exists($this, 'set' . $propertyName)) { return call_user_func(
array($this, 'set' . $propertyName), $value
);
} else {
//Если значение свойства действительно было изменено, //но еще не попало в массив changedProperties,
//то добавить его. if($this->_properties[$propertyName] != $value &&
!in_array($propertyName, $this->_changedProperties)) { $this->_changedProperties[] = $propertyName;
}
//Устанавливаем значение свойства $this->_properties[$propertyName] = $value;
}
}
//запрещаем изменение значения пользовательского идентификатора
function setID($value) {
throw new Exception('Изменение пользовательского идентификатора не допускается!');
}
function sayHello() {
print "Привет! Меня зовут {$this->realname}. Мой идентификатор: {$this->id}";
}
Введение в объектно*ориентированное программирование 501
function __destruct() {
//Проверяем наличие изменений. Если они есть, //то сохраняем из в базе данных.
if(sizeof($this->_changedProperties)) {
$sql = "UPDATE users SET ";
//Формируем SQL-оператор путем создания
//массива операторов и их последующего объединения. //В конце каждого оператора добавляется запятая.
$setStatements = array(); foreach($this->_changedProperties as $prop) {
$setStatements[] = "$prop = '{$this->_properties[$prop]}'";
}
//создаем строку
$sql .= join(', ', $setStatements);
//присоединяем предложение WHERE
$sql .= " WHERE userid = $this->id"; $hRes = mysql_query($sql);
}
//Закрываем подключение к базе данных – все закончено. mysql_close($this->_hDB);
}
}
Измените файл testuser.php, как показано ниже:
<?php require_once('class.User.php');
$obj = new User(27); //Пользователь Роберт Смит
$obj->realname = 'Боб Смит';
$obj->sayHello();
$obj2 = new User(34); //Джейн Доу $obj2->sayHello();
?>
Как это работает
Сначала создается новая частная переменная экземпляра, которая называется $_changedProperties. В метод __set вносятся изменения, позволяющие сохранять в этой переменной изменения любых свойств. Когда эта функция вызывается, прове+ ряется как имя свойства, так и его и его значение. Если новое значение отличается от старого, то изменение регистрируется и свойству присваивается новое значение. Нет необходимости регистрировать изменение более одного раза, поэтому также прове+ ряется, не было ли данное свойство уже добавлено в массив $_changedProperties.