<?php
/**
* Пример реализации Generic-класса
*
* Возможности:
* 1. Осуществляет доступ к параметрам класса через методы get* set* (accessors/mutators)
* Пример: $object->setName('Dima'); $object->getName();
* При изменении стандартного поведения метода (добавление дополнительной логики)
* можно просто определить его в классе при этом не изменяя интерфейс класса.
* 2. Поддерживает встроенную валидацию данных с возможностью использовать или заранее
* определенные типы данных (numeric, string, email, date и др.)
* или регулярные выражения.
* 3. Имплементирует паттерн Lazy initialization для инициализации объектов.
* Позволяет загружать параметры объекта из БД (например) не сразу при инициализации,
* а только при первом запросе.
*
* Видимые недостатки:
* 1. Отсутствует возможность использовать автокомплит в IDE, так как методы явно не определены
*
* Плюсы:
* 1. Позволяет избежать дублирования кода при определении однотипных методов set и get
* 2. Сокращает код классов и централизует управление параметрами
* 3. При необходимости можно расширить функциональность. Например, добавив callback функции
* или принудительную фильтрацию данных.
* 4. Избавляет от рутинных обязанностей проверки входящих данных, тем самым
* обеспечивая некоторый уровень безопасности.
* Это совсем не дает 100% гарантии — не стоит забывать об этом.
* 5. Не вносит коррективы в интерфейс классов. Можно легко сочетать обычные классы и классы
* наследующие GenericObject без видимых различий для клиентов класса. При необходимости
* можно легко отказаться от использования просто переопределив все гетеры и сетеры.
* 6. Упрощает работу с классами, предоставляя удобный и естественный способ
* инициализации объектов:
* $obj1 = new Class(123);
* $obj2 = new Class(array(
* 'param1' => 'value1',
* 'param2' => 'value2')
* ));
*/
abstract class GenericObject
{
/**
* Массив, содержащий все параметры класса
* Необходимо переопределять в каждом классе-наследнике
* var array
*/
protected $_params = array();
/**
* Флаг выполнения и параметр инициализации
*/
private
$_initParam = null,
$_alreadyInitiated = false;
/**
* Инициализация объекта
*
* В зависимости от переданных параметров инициализируем объект:
* 1. передан string|integer: запоминаем параметр и используем его для инициализации
* данных при первом обращении к ним (Lazy Initialization).
* 2. передан array: циклически достаем данные из массива, проверяем есть ли такие параметры
* в классе, валидируем и добавляем их в объект.
*
* param mixed $property
*/
public function __construct($property = null)
{
if ($property === null) return;
if (is_scalar($property)) {
$this->_initParam = $property;
} elseif (is_array($property)) {
$this->_alreadyInitiated = true;
$this->_setParams($property);
}
}
/**
* Функция осуществляет инициализацию объекта класса
* Следует переопределять в каждом классе-наследнике
*
* param mixed $initParam — ключ для получения информации о объекте
*/
protected function _init($initParam)
{
}
/**
* Перехват вызова несуществующего метода
* Если имя вызываемого метода начинается с get или set — находим соответствующий
* параметр и возвращаем или присваиваем ему новое значение
*/
public function __call($name, array $args)
{
if (strlen($name) > 3) {
$paramName = strtolower($name{3}). substr($name, 4);
switch (substr($name, 0, 3)) {
case 'get':
return $this->_getParam($paramName);
case 'set':
if (empty($args)) {
throw new GenericObject_Exception('Не указано значение для присваивания параметру');
}
if ($this->_setParam($paramName, $args[0])) {
return $this;
}
}
}
throw new GenericObject_Exception('Вызван отсутствующий метод класса '. __CLASS__. '::'. $name. '()');
}
/**
* Присвоение значения параметру с предварительной валидацией
*/
protected function _setParam($name, $value)
{
if (!array_key_exists($name, $this->_params)) {
return false;
}
$type = !empty($this->_params[$name]['type']) ? $this->_params[$name]['type'] : null;
$regex = !empty($this->_params[$name]['regex']) ? $this->_params[$name]['regex'] : null;
/**
* Валидация значения, если указаны тип или регулярное выражение для проверки
* Сам класс отвечающий за валидацию мы рассматривать не будем.
* Важно лишь знать, что он принимает на вход значение, тип и, если есть, регульрное выражение.
* И возвращает результат валидации.
*/
if ($value !== null && ($type !== null || $regex !== null)) {
if (!Validate::check($value, $type, $regex)) {
if (isset($this->_params[$name]['message'])) {
throw new Validate_Exception($this->_params[$name]['message']);
}
throw new Validate_Exception('Неверно указан параметр '. $name .' ('. ($type ? $type : $regex) .')');
}
}
$this->_params[$name]['value'] = $value;
return true;
}
/**
* Возвращает значения
* Если значение параметра не определено — вызывается метод инициализации.
* Если и после инициализации значение параметра не появилось — возвращает null.
*/
protected function _getParam($name)
{
if (!$this->_issetParam($name)) {
$this->_callInitMethod();
}
return $this->_issetParam($name) ? $this->_params[$name]['value'] : null;
}
/**
* Установка и валидация параметров из ассоциативного массива
*
* param array $params = array('paramName' => 'value', ...)
*/
protected function _setParams(array $params)
{
foreach ($params as $name => $value) {
$this->_setParam($name, $value);
}
}
/**
* Проверка на наличие установленного значения у параметра
* return bool
*/
protected function _issetParam($paramName)
{
return isset($this->_params[$paramName]['value']);
}
/**
* Вызываем функцию инициализации
* Инициализация происходить только один раз и только при наличии
* параметра инициализации (например идентификатора записи в БД)
*/
private function _callInitMethod()
{
if ($this->_alreadyInitiated === true || $this->_initParam === null) {
return;
}
$this->_alreadyInitiated = true;
$this->_init($this->_initParam);
}
}
/**
* Пример класса-наследника
*/
class Customer extends GenericObject implements Customer_Interface
{
protected $_params = array(
'id' => array('type' => 'numeric'),
'name' => array('type' => 'string'),
'email' => array('type' => 'email'),
'phone' => array('regex' => '/^[\(\)\s\d\-]+$/', 'message' => 'Неверно указан телефон'),
'address' => array('type' => 'string'),
);
protected function _init($customerId)
{
// данные достаются, например из БД
$params = array(
'id' => '777',
'name' => 'Jon',
'email' => 'jon.black@gmail.com',
'phone' => '123-45-67',
'address' => 'Green street, 3/1',
);
$this->_setParams($params);
}
/**
* Мы можем явно переопределить любой метод для добавления дополнительной бизнес-логики
* При этом нам не понадобиться менять интерфейс класса.
*/
public function setEmail($email)
{
if (!$this->_checkIsUniqCustomerEmail($email)) {
throw Validate_Exception('Заказчик с таким адресом эл. почты уже есть в базе данных!');
}
$this->_setParam('email', $email);
}
/**
* Реализуем процесс сохранения данных объекта.
* Если указан id — апдейтим, если нет — инсертим, и присваиваем новый идентификатор объекту
*/
public function save()
{
if ($this->_issetParam('id')) {
$this->_update();
} else {
$newId = $this->_insert();
$this->_setParam('id', $newId);
}
}
}
// В момент инициализации не происходит обращение к базе данных.
// Мы можем, например, создать таким образом большое кол-во объектов,
// а использовать только те из них, которые нам необходимы, при этом не выполняя большое кол-во запросов к БД.
$customer1 = new Customer(123);
$customer2 = new Customer(777);
$customer3 = new Customer(444);
print $customer2->getName(); // Jon
// Поддерживает fluent interface, т.е. каждый сетер возвращает ссылку на содержащий
// его объект, тем самым позволяя поддерживать следующий стиль написания:
$customer3->setName('Key')
->setEmail('key@gmail.com')
->setPhone('555-55-55')
->save();
try {
$customer1->setPhone('incorrect value'); // @throws Validate_Exception;
} catch (Validate_Exception $e) {
print $e->getMessage();
}
// Если нам нужно создать новый объект класса, мы может поступить так:
$customerNew = new Customer(array(
'name' => $_POST['customerName'],
'email' => $_POST['customerEmail'],
'phone' => $_POST['customerPhone'],
'address' => $_POST['customerAddress'],
));
// допустим у нас есть метод save, который сохраняет объект в БД
$customerNew->save();
// после этого мы можем узнать id только что сохраненного объекта
print $customerNew->getId();
Конструктивная (!) критика приветствуется.
/**
* Пример реализации Generic-класса
*
* Возможности:
* 1. Осуществляет доступ к параметрам класса через методы get* set* (accessors/mutators)
* Пример: $object->setName('Dima'); $object->getName();
* При изменении стандартного поведения метода (добавление дополнительной логики)
* можно просто определить его в классе при этом не изменяя интерфейс класса.
* 2. Поддерживает встроенную валидацию данных с возможностью использовать или заранее
* определенные типы данных (numeric, string, email, date и др.)
* или регулярные выражения.
* 3. Имплементирует паттерн Lazy initialization для инициализации объектов.
* Позволяет загружать параметры объекта из БД (например) не сразу при инициализации,
* а только при первом запросе.
*
* Видимые недостатки:
* 1. Отсутствует возможность использовать автокомплит в IDE, так как методы явно не определены
*
* Плюсы:
* 1. Позволяет избежать дублирования кода при определении однотипных методов set и get
* 2. Сокращает код классов и централизует управление параметрами
* 3. При необходимости можно расширить функциональность. Например, добавив callback функции
* или принудительную фильтрацию данных.
* 4. Избавляет от рутинных обязанностей проверки входящих данных, тем самым
* обеспечивая некоторый уровень безопасности.
* Это совсем не дает 100% гарантии — не стоит забывать об этом.
* 5. Не вносит коррективы в интерфейс классов. Можно легко сочетать обычные классы и классы
* наследующие GenericObject без видимых различий для клиентов класса. При необходимости
* можно легко отказаться от использования просто переопределив все гетеры и сетеры.
* 6. Упрощает работу с классами, предоставляя удобный и естественный способ
* инициализации объектов:
* $obj1 = new Class(123);
* $obj2 = new Class(array(
* 'param1' => 'value1',
* 'param2' => 'value2')
* ));
*/
Реализация
abstract class GenericObject
{
/**
* Массив, содержащий все параметры класса
* Необходимо переопределять в каждом классе-наследнике
* var array
*/
protected $_params = array();
/**
* Флаг выполнения и параметр инициализации
*/
private
$_initParam = null,
$_alreadyInitiated = false;
/**
* Инициализация объекта
*
* В зависимости от переданных параметров инициализируем объект:
* 1. передан string|integer: запоминаем параметр и используем его для инициализации
* данных при первом обращении к ним (Lazy Initialization).
* 2. передан array: циклически достаем данные из массива, проверяем есть ли такие параметры
* в классе, валидируем и добавляем их в объект.
*
* param mixed $property
*/
public function __construct($property = null)
{
if ($property === null) return;
if (is_scalar($property)) {
$this->_initParam = $property;
} elseif (is_array($property)) {
$this->_alreadyInitiated = true;
$this->_setParams($property);
}
}
/**
* Функция осуществляет инициализацию объекта класса
* Следует переопределять в каждом классе-наследнике
*
* param mixed $initParam — ключ для получения информации о объекте
*/
protected function _init($initParam)
{
}
/**
* Перехват вызова несуществующего метода
* Если имя вызываемого метода начинается с get или set — находим соответствующий
* параметр и возвращаем или присваиваем ему новое значение
*/
public function __call($name, array $args)
{
if (strlen($name) > 3) {
$paramName = strtolower($name{3}). substr($name, 4);
switch (substr($name, 0, 3)) {
case 'get':
return $this->_getParam($paramName);
case 'set':
if (empty($args)) {
throw new GenericObject_Exception('Не указано значение для присваивания параметру');
}
if ($this->_setParam($paramName, $args[0])) {
return $this;
}
}
}
throw new GenericObject_Exception('Вызван отсутствующий метод класса '. __CLASS__. '::'. $name. '()');
}
/**
* Присвоение значения параметру с предварительной валидацией
*/
protected function _setParam($name, $value)
{
if (!array_key_exists($name, $this->_params)) {
return false;
}
$type = !empty($this->_params[$name]['type']) ? $this->_params[$name]['type'] : null;
$regex = !empty($this->_params[$name]['regex']) ? $this->_params[$name]['regex'] : null;
/**
* Валидация значения, если указаны тип или регулярное выражение для проверки
* Сам класс отвечающий за валидацию мы рассматривать не будем.
* Важно лишь знать, что он принимает на вход значение, тип и, если есть, регульрное выражение.
* И возвращает результат валидации.
*/
if ($value !== null && ($type !== null || $regex !== null)) {
if (!Validate::check($value, $type, $regex)) {
if (isset($this->_params[$name]['message'])) {
throw new Validate_Exception($this->_params[$name]['message']);
}
throw new Validate_Exception('Неверно указан параметр '. $name .' ('. ($type ? $type : $regex) .')');
}
}
$this->_params[$name]['value'] = $value;
return true;
}
/**
* Возвращает значения
* Если значение параметра не определено — вызывается метод инициализации.
* Если и после инициализации значение параметра не появилось — возвращает null.
*/
protected function _getParam($name)
{
if (!$this->_issetParam($name)) {
$this->_callInitMethod();
}
return $this->_issetParam($name) ? $this->_params[$name]['value'] : null;
}
/**
* Установка и валидация параметров из ассоциативного массива
*
* param array $params = array('paramName' => 'value', ...)
*/
protected function _setParams(array $params)
{
foreach ($params as $name => $value) {
$this->_setParam($name, $value);
}
}
/**
* Проверка на наличие установленного значения у параметра
* return bool
*/
protected function _issetParam($paramName)
{
return isset($this->_params[$paramName]['value']);
}
/**
* Вызываем функцию инициализации
* Инициализация происходить только один раз и только при наличии
* параметра инициализации (например идентификатора записи в БД)
*/
private function _callInitMethod()
{
if ($this->_alreadyInitiated === true || $this->_initParam === null) {
return;
}
$this->_alreadyInitiated = true;
$this->_init($this->_initParam);
}
}
Пример использования
/**
* Пример класса-наследника
*/
class Customer extends GenericObject implements Customer_Interface
{
protected $_params = array(
'id' => array('type' => 'numeric'),
'name' => array('type' => 'string'),
'email' => array('type' => 'email'),
'phone' => array('regex' => '/^[\(\)\s\d\-]+$/', 'message' => 'Неверно указан телефон'),
'address' => array('type' => 'string'),
);
protected function _init($customerId)
{
// данные достаются, например из БД
$params = array(
'id' => '777',
'name' => 'Jon',
'email' => 'jon.black@gmail.com',
'phone' => '123-45-67',
'address' => 'Green street, 3/1',
);
$this->_setParams($params);
}
/**
* Мы можем явно переопределить любой метод для добавления дополнительной бизнес-логики
* При этом нам не понадобиться менять интерфейс класса.
*/
public function setEmail($email)
{
if (!$this->_checkIsUniqCustomerEmail($email)) {
throw Validate_Exception('Заказчик с таким адресом эл. почты уже есть в базе данных!');
}
$this->_setParam('email', $email);
}
/**
* Реализуем процесс сохранения данных объекта.
* Если указан id — апдейтим, если нет — инсертим, и присваиваем новый идентификатор объекту
*/
public function save()
{
if ($this->_issetParam('id')) {
$this->_update();
} else {
$newId = $this->_insert();
$this->_setParam('id', $newId);
}
}
}
// В момент инициализации не происходит обращение к базе данных.
// Мы можем, например, создать таким образом большое кол-во объектов,
// а использовать только те из них, которые нам необходимы, при этом не выполняя большое кол-во запросов к БД.
$customer1 = new Customer(123);
$customer2 = new Customer(777);
$customer3 = new Customer(444);
print $customer2->getName(); // Jon
// Поддерживает fluent interface, т.е. каждый сетер возвращает ссылку на содержащий
// его объект, тем самым позволяя поддерживать следующий стиль написания:
$customer3->setName('Key')
->setEmail('key@gmail.com')
->setPhone('555-55-55')
->save();
try {
$customer1->setPhone('incorrect value'); // @throws Validate_Exception;
} catch (Validate_Exception $e) {
print $e->getMessage();
}
// Если нам нужно создать новый объект класса, мы может поступить так:
$customerNew = new Customer(array(
'name' => $_POST['customerName'],
'email' => $_POST['customerEmail'],
'phone' => $_POST['customerPhone'],
'address' => $_POST['customerAddress'],
));
// допустим у нас есть метод save, который сохраняет объект в БД
$customerNew->save();
// после этого мы можем узнать id только что сохраненного объекта
print $customerNew->getId();
Post scriptum
Данный код не претендует на истину в последней инстанции, а является лишь одним из многочисленных примеров реализации подобной функциональности. Автор не предлагает использовать его повсеместно, т.к. иногда в этом просто нет необходимости (например, при использовании ORM библиотек).Конструктивная (!) критика приветствуется.