PHP

Сеттеры в PHP. Правильное использование.

В объектно-ориентированном программировании особое внимание уделяется инкапсуляции данных. В PHP, как и в других ОО-языках для этого существует область видимости данных и методов класса. Несмотря на всю полезность private, protected и public механизмов определения данных, для разработки полноценных приложений одного объявления уровня доступа к данным не достаточно.

Возьмем в пример тривиальный объект User с public-свойствами класса:
<?php
class User
{
// Имя пользователя.
public $name;
 
// Дата рождения пользователя.
public $age;
 
// Пол пользователя.
public $sex;
 
// URL домашней страницы пользователя.
public $homepage_url;
 
public function __construct(){/* ... */}
}

Программист, написавший данный класс, оставил свойства класса открытыми. Нарушил ли он инкапсуляцию? Трудно сказать, ответ может быть и "да" и "нет".
С теоретической точки зрения это правильный код и он вполне будет работать:
<?php
$user = new User();
$user->name = 'Вася Иванов';
$user->age = '18-08-1982';
$user->sex = 'Male';
$user->homepage_url = 'http://domain.su';

Но с точки зрения практики это плохой код. И плохой он в первую очередь из-за отсутствия методов, позволяющих принимать даные в объект класса и возвращать их из него. Зачем могут потребоваться такие методы? Не секрет, что основной успех ООП - это возможность держать в объектах и свойства и методы, оперирующие этими свойствами. Объект User вполне может иметь требования к входящим данным, а клиент, запрашивающий данные от объекта User - к исходящим.

Например, одним из требований может быть невозможность помещать в свойство $age значение, отличное от объекта Datetime или дату рождения в неверном формате. Свойство $sex может иметь только одно из двух значений - 'Male' или 'Female'. Cвойство $name не должно быть пустыми, а свойство $homepage_url должно содержать валидный URL адрес.
Клиент, запрашивающий данные, также может иметь требования к свойствам класса, такие как возможность получать из объекта User свойство $age в различных форматах даты.

Все эти требования диктуют пересмотреть архитектуру класса User и создать методы, с помощью которых мы будем в объект класса User устанавливать (set) и получать (get) различные значения.

Явные методы доступа к данным
Перепишем класс User. Во-первых, сделаем все свойства класса защищёнными, указав уровень доступа как protected, а во-вторых дополнив его методами, оперирующими его защищёнными свойствами:
class User
{
// Имя пользователя.
protected $name;
 
// Возраст пользователя.
protected $age;
 
// Пол пользователя.
protected $sex;
 
// URL домашней страницы пользователя.
protected $homepage_url;
 
public function __construct(){}
 
public function setName($name) {
$name = trim($name);
if ($name == '') {
throw new Exception('Не указано имя пользователя');
}
$this->name = $name;
 
return $this;
}
 
public function getName() {
return $this->name;
}
 
public function setAge($age) {
if (is_object($age) && $age instanceof Datetime) {
$this->age = $age;
} else {
try {
$age = new Datetime($age);
$this->age = $age;
} catch (Exception $e) {
throw new Exception('Некорректный формат даты рождения пользователя');
}
}
 
return $this;
}
 
public function getAge() {
return $this->age;
}
 
public function setSex($sex) {
if (!in_array($sex, array('male', 'female'))) {
throw new Exception('Неопределённый пол пользователя');
}
$this->sex = $sex;
 
return $this;
57.
}
58.
 
public function getSex() {
return $this->sex;
}
 
public function setHomepageUrl($url) {
$this->homepage_url = $url;
 
return $this;
}
 
public function getHomepageUrl() {
return $this->homepage_url;
}
}

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

указать в качестве аргумента метода User::setName() пустую строку
указать в качестве аргумента метода User::setAge() некорректный формат даты, типа 18-088-1982
указать в качестве аргумента метода User::setSex() строку, отличную от значений 'male' и 'female'
то исключения, сработавшие в этих методах, передадут клиентскому коду соответствующее сообщение об ошибке. Попробуем указать неверный формат даты рождения пользователя в виде строки '18-088-1982':
try {
$user = new User();
$user->setName('Вася Иванов')
->setAge('18-088-1982')
->setSex('male')
->setHomepageUrl('http://www.domain.su');
} catch (Exception $e) {
echo "Ошибка присвоения данных: " . $e->getMessage();
}

В результате в методе User::setAge() сработает сначала стандартное исключение объекта Datetime, которое будет перехвачено конструкцией try-catch, после чего будет выброшено исключение с человекопонятным описанием ошибки:

Ошибка присвоения данных: Некорректный формат даты рождения пользователя

На этом маленьком примере виден один из столпов объектно-ориентированного программирования - инкапсуляция. Теперь объект пользователя не только хранит в себе данные, но и умеет их валидировать, гарантируя, что объект класса User всегда содержит корректные данные. Фактически, мы создали модель (в рамках терминологии MVC) данных типа "пользователь". Теперь клиентский код может оперировать объектом пользователя:
try {
$user = new User();
$user->setName('Вася Иванов')
->setAge('18-08-1982')
->setSex('male')
->setHomepageUrl('http://www.phpinfo.su');
} catch (Exception $e) {
echo "Ошибка присвоения данных: " . $e->getMessage();
}
 
echo "Привет, " . $user->getName() . "! ".
"Ты родился в " . $user->getAge()->format('Y') . " году. " .
"Твой пол - " . $user->getSex() . ". " .
"Твой URL: " . $user->getHomepageUrl();

Выдаст

Привет, Вася Иванов! Ты родился в 1982 году. Твой пол - male. Твой URL: http://www.domain.su


Обратите внимание, как мы получили год рождения пользователя - метод User::getAge() вернул объект класса Datetime, который хранит информацию о дате. У объекта класса Datetime есть метод format, который и отформатировал дату рождения пользователя в нужном нам формате. Очень элегантно и крайне "ООПшно"!

Магия PHP
Представим, что класс User содержал бы в себе не 4, а скажем 10 - 15 свойств. 15 умножить на 2 (set- и get- метод для каждого свойства), получаем 30 методов! Нам пришлось бы написать 30 методов в классе! Если посмотреть внимательно на вышеприведённый класс User, то можно заметить, что не все методы имеют логику внутри себя:

set-метод User::setHomepageUrl() только присваивает переданный аргумент в $this->homepage_url, никаких проверок на валидность URL адреса мы не реализовали.
get-методы вообще не содержат никакой логики, их главная задача - отдать определённое значение.
Что бы не писать кучу однотипных методов для десятка свойств в классе, мы можем использовать существующий в PHP магический метод __call(). Данный метод, объявленный в классе, вызывается всякий раз, когда у объекта запрашивается несуществующий метод. API метода выглядит так:
public mixed __call(string $name, array $arguments)

$name - имя вызываемого несуществующего метода объекта
$arguments - аргументы, с которыми был вызван несуществующий метод объекта
Таким образом, при вызове несуществующего метода объекта класса User::setUserFriend() (установить друга пользователя)
$user->setUserFriend($friend);

запустится магический метод __call(), первым аргументом которого будет строка "setUserFriend", а вторым - массив, содержащий значение $friend. Имея эти данные, мы можем понять, что вызов несуществующего метода User::setUserFriend($friend) говорит нам о том, что нужно некое значение $friend записать (set) в свойство объекта $user_friend. Наша задача заключается только в том, что бы "распарсить" строку "setUserFriend" на составляющие:

Понять, что какое действие выполнять - присваивание (set) или получение (get)
Понять, какое свойство объекта задействовать. В данном случае - свойство под названием $user_friend.
Перепишем класс User с учётом новых требований:
class User
{
// Имя пользователя.
protected $name;
 
// Возраст пользователя.
protected $age;
 
// Пол пользователя.
protected $sex;
 
// URL домашней страницы пользователя.
protected $homepage_url;
 
public function __construct(){}
 
/**
* Получение и установка свойств объекта через вызов магического метода вида:
19.
* $object->(get|set)PropertyName($prop);
*
* @see __call
* @return mixed
*/
public function __call($method_name, $argument)
{
$args = preg_split('/(?<=\w)(?=[A-Z])/', $method_name);
$action = array_shift($args);
$property_name = strtolower(implode('_', $args));
 
switch ($action)
{
case 'get':
return isset($this->$property_name) ? $this->$property_name : null;
 
case 'set':
$this->$property_name = $argument[0];
return $this;
}
}
 
public function setName($name) {
$name = trim($name);
if ($name == '') {
throw new Exception('Не указано имя пользователя');
}
$this->name = $name;
 
return $this;
}
 
public function setAge($age) {
if (is_object($age) && $age instanceof Datetime) {
$this->age = $age;
} else {
try {
$age = new Datetime($age);
$this->age = $age;
} catch (Exception $e) {
throw new Exception('Некорректный формат даты рождения пользователя');
}
}
 
return $this;
}
 
public function setSex($sex) {
if (!in_array($sex, array('male', 'female'))) {
throw new Exception('Неопределённый пол пользователя');
}
$this->sex = $sex;
 
return $this;
}
}

Как видно, мы убрали все get-методы получения данных и убрали метод User::setHomepageUrl(), определили в классе User метод __call(), который занимается определением нужного действия и определением имени свойства объекта, к которому применить действие. Для того, что бы имя свойства было определено верно, несуществующий метод нужно писать в CamelCase стиле:
setHomepageUrl, getHomepageUrl
setName, getName
setAge, getAge
setSex, getSex

Строка "HomepageUrl" будет преобразована в имя свойства "homepage_url".
Строка "Name" будет преобразована в имя свойства "name" и т.д.

Соответственно, имена свойств определяются в классе в нижнем регистре, слова разделяются знаком нижнее подчёркивание "_":
$homepage_url
$name
$age
$sex

Взглянем на результат:
try {
$user = new User();
$user->setName('Вася Иванов')
->setAge('18-08-1982')
->setSex('male')
->setHomepageUrl('http://www.domain.su');
} catch (Exception $e) {
echo "Ошибка присвоения данных: " . $e->getMessage();
}
 
echo "Привет, " . $user->getName() . "! ".
"Ты родился в " . $user->getAge()->format('Y') . " году. " .
"Твой пол - " . $user->getSex() . ". " .
"Твой URL: " . $user->getHomepageUrl();

Код отработал как нужно, несмотря на то, что в нём нет явно объявленных get-методов для получения данных, а также нет метода User::setHomepageUrl(). Всю "грязную" работу за нас взял магический метод __call().

Выводы
Используя __call() мы оградили себя от рутинной работы - написания множества однотипных методов, в чьи обязанности входило лишь присвоение свойствам объекта значений и их получение. В случае необходимости, мы можем описать в классе явный метод, это никак не повлияет на клиентский код, уже использующий одноимённый виртуальный метод:
class User
{
// .....
 
/**
* Получить URL-адрес без строки протокола.
*
* @return string
*/
public function getHomepageUrl() {
return str_ireplace('http://', '', $this->homepage_url);
}  
}
// Клиентский код, который ранее использовал виртуальный метод.
// Введение в класс явно описанного метода User::getHomepageUrl()
// никак не повлияло на работу кода.
echo $user->getHomepageUrl();

Результат:

www.domain.su
Теги:
сеттеры, ооп
Добавлено: 14 Сентября 2013 11:01:51 Добавил: Андрей Ковальчук Нравится 0
Добавить
Комментарии:
Нету комментариев для вывода...