Сложно представить достаточно крупный проект, в котором не было бы работы с моделями данных. Скажу больше: по моему опыту около трех четвертых всего кода — это создание, загрузка, изменение, сохранение или удаление записей. Будь то регистрация пользователя, вывод десятка последних статей или работа с админкой — все это мелкая работа с базовыми операциями моделей. И, соответственно, такой код должен писаться и читаться быстро и не должен забивать голову программиста техническими деталями: он (программист) должен думать о логике работы приложения, а не об очередном UPDATE-запросе.
Вместо предисловия
На мой взгляд, чтобы код легко читался, он (помимо, разумеется, стандартов кодирования и понятности алгоритмов) должен быть максимально приближен к естественному языку. Загрузи этот товар, установи ему вот такое название, установи вот такую цену, сохрани. Кроме того, если в коде возможно избежать от повторения, будь то кусок кода или просто название переменной (при работе с одним объектом), то его следует избежать. В моем случае «текучий интерфейс» избавил меня от постоянного нудного копирования имени переменной.
Текучий интерфейс
Логично будет отделить мух от котлет и вынести «текучий интерфейс» в отдельный класс, на случай, если его потребуется использовать не только в моделях:
abstract class Core_Fluent extends ArrayObject {}
Перед тем, как начать писать, я определился, каким я хочу видеть конечный код, который я буду использовать. Получилось вот это:
$instance->load($entity_id) ->setName('Foo') ->setDescription('Bar') ->setBasePrice(250) ->save();
При этом, я хотел, чтобы данные хранились с ключами вида «name», «description», «base_price» (это позволило бы гораздо проще реализовать взаимодействие с БД и этого требовал мой стандарт кодирования).
Для того, чтобы не писать в каждой модели однотипные методы, следует использовать «магические методы» (Magic Methods), в частности, метод __call(). Также можно было использовать методы __get() и __set() , но я пошел путем применения ArrayIterator.
Итак, метод __call, который будет определять, что именно было вызвано и что вообще дальше делать:
... // регулярное выражение для преобразования CamelCase в стиль_через_подчеркивания // Взял в свое время со StackOverflow, потому как в регулярках не силен const PREG_CAMEL_CASE = '/(?<=[A-Z])(?=[A-Z][a-z])|(?<=[^A-Z])(?=[A-Z])|(?<=[A-Za-z])(?=[^A-Za-z])/'; // Массив для хранения самих данных protected $_data = array(); public function __call($method_name, array $arguments = array()) { // Первым делом проверяем, подходит ли вообще то, что было вызвано под используемый // шаблон и делим на действие и свойство (setBasePrice тут разделится на set и BasePrice) if(!preg_match('/^(get|set|isset|unset)([A-Za-z0-9]+)$/', $method_name, $data)) { // И если не подходит, то бросаем исключение throw new Core_Exception('Method '.get_called_class().'::'.$method_name.' was not found'); } // Затем переводим имя свойства в стандартный вид (BasePrice => base_price) и помещаем в $property $property = strtolower(preg_replace(self::PREG_CAMEL_CASE, '_$0', $data[2])); // Теперь надо понять, что с этим вообще делать switch($data[1]) { case 'get': { // $object->getBasePrice(): возвращаем значение свойства return $this->get($property); } break; case 'set': { // $object->setBasePrice(): изменяем значение свойства return $this->set($property, $arguments[0]); } break; case 'unset': { // $object->getBasePrice(): удаляем свойство из объекта return $this->_unset($property); } break; case 'isset': { // $object->getBasePrice(): проверяем, есть ли это свойство у объекта return $this->_isset($property); } break; default: { } } // И, если мы сюда дошли, то возвращаем объект, реализуя таким образом "текучий интерфейс" return $this; } ...
Методы get, set, _isset и _unset
Реализация этих методов не представляет никакой сложности, их действие очевидно из названия:
... public function get($code) { if($this->_isset($code)) { return $this->_data[$code]; } // Вот тут можно бросить исключение, но я предпочел просто вернуть NULL return NULL; } public function set($code, $value) { $this->_data[$code] = $value; return $this; } public function _unset($code) { unset($this->_data[$code]); return $this; } public function _isset($code) { return isset($this->_data[$code]); } ...
ArrayIterator
Помимо вышеозначенного подхода, я решил добавить возможность работать с объектом и как с обычным ассоциативным (и не только, но это уже другая история) массивом: для этого есть ArrayIterator. Конечно, правильнее было назвать методы, описанные в предыдущем разделе, так, чтобы не пришлось дублировать, но, во-первых, тут уже пришлось думать об обратной совместимости, поскольку был код, использующий эти методы напрямую и его было достаточно много, а во-вторых, на мой взгляд, одно дело — реализация ArrayIterator, а другое — реализация текучего интерфейса.
... public function offsetExists($offset) { return $this->_isset($offset); } public function offsetUnset($offset) { return $this->_unset($offset); } public function offsetGet($offset) { return $this->get($offset); } public function offsetSet($offset, $value) { return $this->set($offset, $value); } public function getIterator() { return new Core_Fluent_Iterator($this->_data); } ...
И, соответственно, класс Core_Fluent_Iterator:
class Core_Fluent_Iterator extends ArrayIterator {}
Все. Теперь с любым классом, наследующимся от Core_Fluent доступны такие манипуляции:
class Some_Class extends Core_Fluent {} $instance = new Some_Class(); $instance->set('name', 'Foo')->setDescription('Bar')->setBasePrice(32.95); echo $instance->getDescription(), PHP_EOL; // Bar echo $instance['base_price'], PHP_EOL; // 32.95 echo $instance->get('name'), PHP_EOL; // Foo // name => Foo // description => Bar // base_price => 32.95 foreach($instance as $key => $value) { echo $key, ' => ', $value, PHP_EOL; } var_dump($instance->issetBasePrice()); // true var_dump($instance->issetFinalPrice()); // false var_dump($instance->unsetBasePrice()->issetBasePrice()); // false
Модель
Теперь сама модель, частный случай применения вышеописанного механизма.
abstract class Core_Model_Abstract extends Core_Fluent {}
Для начала необходимо добавить основу для CRUD (создание, загрузка, изменение и удаление). Логика (работа с БД, файлами и чем угодно еще) будет ниже по иерархии, здесь нужно сделать только самое основное:
... // Массив измененных свойств, понадобится чуть позже protected $_changed_properties = array(); // Создание. При реализации save() ние по иерархии можно добавить проверку на // существование и вызывать этот метод автоматически, в случае если идентификатор // не найден в базе (или где угодно еще) public function create() { return $this; } // Загрузка public function load($id) { $this->_changed_properties = array(); return $this; } // Загрузка из массива public function loadFromArray(array $array = array()) { $this->_data = $array; return $this; } // Сохранение public function save() { $this->_changed_properties = array(); return $this; } // Удаление public function remove() { return $this->unload(); } // Выгрузка из памяти public function unload() { $this->_changed_properties = array(); $this->_data = array(); return $this; } // Конвертация объекта в массив public function toArray() { return $this->_data; } ...
И, наконец, переопределим set(), добавив массив измененных свойств
... public function set($code, $value) { $this->_changed_properties[] = $code; return parent::set($code, $value); } ...
Теперь от этого класса можно наследовать различные адаптеры к базам данных, файлам или API, от которых, в свою очередь, наследовать уже конечные модели данных.
Полный код всех трех файлов под спойлером.
<?php abstract class Core_Fluent extends ArrayObject { const PREG_CAMEL_CASE = '/(?<=[A-Z])(?=[A-Z][a-z])|(?<=[^A-Z])(?=[A-Z])|(?<=[A-Za-z])(?=[^A-Za-z])/'; protected $_data = array(); public function __call($method_name, array $arguments = array()) { if(!preg_match('/^(get|set|isset|unset)([A-Za-z0-9]+)$/', $method_name, $data)) { throw new Core_Exception('Method '.get_called_class().'::'.$method_name.' was not found'); } $property = strtolower(preg_replace(self::PREG_CAMEL_CASE, '_$0', $data[2])); switch($data[1]) { case 'get': { return $this->get($property); } break; case 'set': { return $this->set($property, $arguments[0]); } break; case 'unset': { return $this->_unset($property); } break; case 'isset': { return $this->_isset($property); } break; default: { } } return $this; } public function get($code) { if($this->_isset($code)) { return $this->_data[$code]; } return NULL; } public function set($code, $value) { $this->_data[$code] = $value; return $this; } public function _unset($code) { unset($this->_data[$code]); return $this; } public function _isset($code) { return isset($this->_data[$code]); } /** * Implementation of ArrayIterator */ public function offsetExists($offset) { return $this->_isset($offset); } public function offsetUnset($offset) { return $this->_unset($offset); } public function offsetGet($offset) { return $this->get($offset); } public function offsetSet($offset, $value) { return $this->set($offset, $value); } public function getIterator() { return new Core_Fluent_Iterator($this->_data); } } ?>
Core/Fluent/Iterator.php
<?php class Core_Fluent_Iterator extends ArrayIterator {} ?>
Core/Model/Abstract.php
<?php abstract class Core_Model_Abstract extends Core_Fluent { protected $_changed_properties = array(); public function set($code, $value) { $this->_changed_properties[] = $code; return parent::set($code, $value); } public function create() { return $this; } public function load($id) { $this->_changed_properties = array(); return $this; } public function loadFromArray(array $array = array()) { $this->_data = $array; return $this; } public function save() { $this->_changed_properties = array(); return $this; } public function remove() { return $this->unload(); } public function unload() { $this->_changed_properties = array(); $this->_data = array(); return $this; } public function toArray() { return $this->_data; } } ?>
Вместо заключения
Получилось достаточно объемно, но, в основном, из-за кода. Если эта тема интересна, то я могу описать реализацию коллекции (некое подобие массива записей с возможностью загрузки с фильтрацией и коллективных (batch) действий) на этом же механизме. И коллекции, и эти модели взяты из разрабатываемого мной фреймворка, поэтому их правильнее рассматривать в комплексе, но я не стал перегружать и без того объемную статью.
Разумеется, буду рад услышать ваше мнение или аргументированную критику.
ссылка на оригинал статьи http://habrahabr.ru/post/185896/
Добавить комментарий