Pull to refresh

GenericObject

Reading time12 min
Views787
<?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();




Post scriptum

Данный код не претендует на истину в последней инстанции, а является лишь одним из многочисленных примеров реализации подобной функциональности. Автор не предлагает использовать его повсеместно, т.к. иногда в этом просто нет необходимости (например, при использовании ORM библиотек).
Конструктивная (!) критика приветствуется.
Tags:
Hubs:
+39
Comments56

Articles