Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 31.05.2014 | нет комментариев

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

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

Способы getset_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, от которых, в свою очередь, наследовать теснее финальные модели данных.

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

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

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

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB