Автоматический «текучий интерфейс» и ArrayIterator в PHP-моделях

от автора

Данный способ не претендует на оригинальность, но, как мне кажется, может быть полезен в понимании принципов работы подобных систем (см. например Varien_Object, написанный разработчиками Magento, идея была взята в первую очередь оттуда) и, возможно, будет полезен в проектах, куда не очень хочется подключать тяжелые фреймворки, но уже нужно как-то систематизировать код.

Сложно представить достаточно крупный проект, в котором не было бы работы с моделями данных. Скажу больше: по моему опыту около трех четвертых всего кода — это создание, загрузка, изменение, сохранение или удаление записей. Будь то регистрация пользователя, вывод десятка последних статей или работа с админкой — все это мелкая работа с базовыми операциями моделей. И, соответственно, такой код должен писаться и читаться быстро и не должен забивать голову программиста техническими деталями: он (программист) должен думать о логике работы приложения, а не об очередном 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, от которых, в свою очередь, наследовать уже конечные модели данных.

Полный код всех трех файлов под спойлером.

Полный код всех трех файлов

Core/Fluent.php

<?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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *