Использование ruby-like модулей в PHP
Недавно мне пришлось работать с очень длинными классами, примерно 1000-1300 строк. Они, знаете ли, не очень удобны для работы. Все время хотелось их разнести по разным модулям, как в ruby, чтобы не мешались в одной куче. А потом подключать эти модули в класс, возможно даже не в один. В итоге я сделал следующее:
Я создал два класса Modularize и Module. От первого я унаследовал классы, которые будут состоять из модулей. От второго – классы-модули, которые я буду подмешивать в классы, потомки Modularize.
class Modularize {
private $delegate = array();
public function __call($method, $args) {
foreach ($this->delegate as $className => $methods) {
if(in_array($method, $methods)) {
$class = $this->getModuleClass($className);
return call_user_func_array(array($class, $method), $args);
}
}
}
public function addModule($className) {
if(!isset($this->delegate[$className])) {
$methods = get_class_methods($className);
$this->delegate[$className] = $methods;
}
}
private function getModuleClass($moduleName) {
$attribName = strtolower($moduleName);
if(!isset($this->$attribName)) {
$this->$attribName = new $moduleName($this);
}
return $this->$attribName;
}
}
class Module {
protected $parent = null;
public function __construct($parent) {
$this->parent = $parent;
}
}
?>
Для проверки работы создал тестовые классы. К примеру, если мы пишем ORM, то они могли бы быть такими: Table, Record.
Вместо одного класса Table со множеством методов, получаем такой:
class Table extends Modularize {
public $name = "Table";
public function __construct() {
$this->addModule("Finder");
$this->addModule("Schema");
$this->addModule("Callbacks");
}
}
?>
И в классы Finder, Schema, Callbacks уберем соответствующие методы из класса Table. Указатель на класс Table передаем в конструктор каждого модуля, поэтому из модуля к нему обратиться можно через закрытое свойство $this->parent.
class Finder extends Module {
protected $queryCache = array();
public function find($type, $queryArray = array()) {
echo "call {$this->parent->name}::find()\n";
}
public function query($sql) {
echo "call {$this->parent->name}::query()\n";
}
}
class Schema extends Module {
private $schema = array();
public function getSchema() {
echo "call {$this->parent->name}::getSchema()\n";
}
public function hasColumn($name) {
echo "call {$this->parent->name}::hasColumn()\n";
}
public function getColumns() {
echo "call {$this->parent->name}::getColumns()\n";
}
public function getColumn($name) {
echo "call {$this->parent->name}::getColumn()\n";
}
public function loadSchema() {
echo "call {$this->parent->name}::loadSchema()\n";
}
public function saveSchema() {
echo "call {$this->parent->name}::saveSchema()\n";
}
}
class Callbacks extends Module {
public function onError() {
echo "call {$this->parent->name}::onError()\n";
}
public function beforeSave() {
echo "call {$this->parent->name}::beforeSave()\n";
}
public function afterSave() {
echo "call {$this->parent->name}::afterSave()\n";
}
public function beforeValidate() {
echo "call {$this->parent->name}::beforeValidate()\n";
}
public function afterValidate() {
echo "call {$this->parent->name}::afterValidate()\n";
}
protected function protectedFunc() {
echo "call {$this->parent->name}::protectedFunc()\n";
}
}
?>
Для класса Record код будет выглядеть так:
class Record extends Modularize {
public $name = "Record";
public function __construct() {
$this->addModule("Finder");
$this->addModule("Validator");
}
}
class Validator extends Module {
private $valid = false;
private $errors = array();
public function validate() {
echo "call {$this->parent->name}::validate()\n";
}
public function isValid() {
echo "call {$this->parent->name}::isValid()\n";
}
public function getErrors() {
echo "call {$this->parent->name}::getErrors()\n";
}
public function getErrorOn($column) {
echo "call {$this->parent->name}::getErrorOn()\n";
}
}
?>
Для проверки написал такой код
$table = new Table("table");
echo "=====================\n";
echo "Table\n";
echo "=====================\n";
echo "---------------------\n";
echo "Finder\n";
echo "---------------------\n";
$table->find("all");
$table->query("SELECT * FROM table");
echo "---------------------\n";
echo "Schema\n";
echo "---------------------\n";
$table->getSchema();
$table->hasColumn("id");
$table->getColumns();
$table->getColumn("id");
$table->loadSchema();
$table->saveSchema();
echo "---------------------\n";
echo "Callbacks\n";
echo "---------------------\n";
$table->onError();
$table->beforeSave();
$table->afterSave();
$table->beforeValidate();
$table->afterValidate();
echo "---------------------\n";
echo "Protected function\n";
echo "---------------------\n";
$table->protectedFunc();
$rec = new Record();
echo "=====================\n";
echo "Record\n";
echo "=====================\n";
echo "---------------------\n";
echo "Finder\n";
echo "---------------------\n";
$rec->find("all");
$rec->query("SELECT * FROM table");
echo "---------------------\n";
echo "Validator\n";
echo "---------------------\n";
$rec->validate();
$rec->isValid();
$rec->getErrors();
$rec->getErrorOn("name");
?>
Результат:
Table
=====================
---------------------
Finder
---------------------
call Table::find()
call Table::query()
---------------------
Schema
---------------------
call Table::getSchema()
call Table::hasColumn()
call Table::getColumns()
call Table::getColumn()
call Table::loadSchema()
call Table::saveSchema()
---------------------
Callbacks
---------------------
call Table::onError()
call Table::beforeSave()
call Table::afterSave()
call Table::beforeValidate()
call Table::afterValidate()
---------------------
Protected function
---------------------
=====================
Record
=====================
---------------------
Finder
---------------------
call Record::find()
call Record::query()
---------------------
Validator
---------------------
call Record::validate()
call Record::isValid()
call Record::getErrors()
call Record::getErrorOn()
Вывод
Pros
- Удобно для разделения больших классов на модули.
- Удобно потом тестировать эти классы.
- Можно одинаковый функционал разных классов выносить в модуль и “подмешивать” его потом в нужные классы (с некоторыми ограничениями, смотри ниже).
Cons
- Классы-модули не имеют доступа к защищенным методам и свойствам класса к которому “подмешаны”. Они должны взаимодействовать через общий интерфейс.
- Класс к которому “подмешан” модуль не имеет доступа к защищенным методам и свойствам класса-модуля.
- Достаточно сложно создать модули, которые можно “подмешивать” к разным классам.
Пример “подмешивания” модуля в разные классы
Например нужно добавить во множество классов возможность сериализовывать данные в JSON. Можно их наследовать от одного и того же класса, но это не всегда удобно. Поэтому можно создать модуль JSONize и подмешать его в требуемые классы. Остается добавить в эти классы методы, позволяющие получить и установить данные объекта в виде массива.
class JSONize extends Module {
public function toJson() {
return json_encode($this->parent->toArray());
}
public function fromJson($json) {
$this->parent->fromArray(json_decode($json, true));
}
}
?>
class SomeObject extends Modularize {
private $data = array("key1" => "value1", "key2" => array("value1", 1, 3));
public function __construct() {
$this->addModule("JSONize");
}
public function toArray() {
return $this->data;
}
public function fromArray($data) {
$this->data = $data;
}
}
?>
class OtherObject extends Modularize {
private $var1 = "value1";
private $var2 = "value2";
private $var3 = array("array_value1", "array_value2");
private $var4 = "value4";
public function __construct() {
$this->addModule("JSONize");
}
public function toArray() {
$array = array();
foreach(array("var1", "var2", "var3", "var4") as $varName) {
$array[$varName] = $this->$varName;
}
return $array;
}
public function fromArray($data) {
foreach(array("var1", "var2", "var3", "var4") as $varName) {
if(isset($data[$varName])) {
$this->$varName = $data[$varName];
}
}
}
}
?>
Для проверки:
$o = new SomeObject();
$o2 = new OtherObject();
echo "=====================\n";
echo "SomeObject\n";
echo "=====================\n";
echo "---------------------\n";
echo "toJson\n";
echo "---------------------\n";
echo $o->toJson() . "\n";
echo "---------------------\n";
echo "fromJson\n";
echo "---------------------\n";
$o->fromJson('{"key":"value","other_key":["other_value",12,33],"bla":"xxx"}');
print_r($o->toArray());
echo "=====================\n";
echo "OtherObject\n";
echo "=====================\n";
echo "---------------------\n";
echo "toJson\n";
echo "---------------------\n";
echo $o2->toJson() . "\n";
echo "---------------------\n";
echo "fromJson\n";
echo "---------------------\n";
$o2->fromJson('{"var1":"value","var2":["other_value",12,33],"var2":"xxx","var4":1000}');
print_r($o2->toArray());
?>
И результат. Все работает.
SomeObject
=====================
---------------------
toJson
---------------------
{"key1":"value1","key2":["value1",1,3]}
---------------------
fromJson
---------------------
Array
(
[key] => value
[other_key] => Array
(
[0] => other_value
[1] => 12
[2] => 33
)
[bla] => xxx
)
=====================
OtherObject
=====================
---------------------
toJson
---------------------
{"var1":"value1","var2":"value2","var3":["array_value1","array_value2"],"var4":"value4"}
---------------------
fromJson
---------------------
Array
(
[var1] => value
[var2] => xxx
[var3] => Array
(
[0] => array_value1
[1] => array_value2
)
[var4] => 1000
)
Можно назвать модуль Serialization и создать методы для XML, YAML, BSON и так далее. Подключаем модуль, добавляем два метода и, вуаля, наш объект умеет сериализовывать себя в разные форматы.
Еще один классический пример с модулем Enumerable
Было бы хорошо, если бы можно было определить класс-модуль Enumerable, содержащий реализацию интерфейсов ArrayAccess, Countable, IteratorAggregate, и “подмешивать” его к разным классам. Но к сожалению, так просто это не получится. Например есть классы:
class EnumerableIterator extends ArrayIterator
{
protected $enumerableArray = null;
public function __construct($enumerableArray) { $this->enumerableArray = $enumerableArray; }
public function current() {
return current($this->enumerableArray);
}
public function key() {
return key($this->enumerableArray);
}
public function next() {
return next($this->enumerableArray);
}
public function rewind() {
reset($this->enumerableArray);
}
public function valid () {
return $this->current() !== false;
}
}
class Enumerable extends Module implements ArrayAccess, Countable, IteratorAggregate {
private $enumerableAarray = array();
public function __construct($parent) {
self::parent($parent);
$this->enumerableArray = &$this->parent->getEnumerableArray();
}
public function getIterator() {
return new InterpolatorIterator($this);
}
public function offsetSet($offset, $value) {
$this->enumerableArray[$offset] = $value;
}
public function offsetExists($offset) {
return isset($this->enumerableArray[$offset]);
}
public function offsetUnset($offset) {
unset($this->enumerableArray[$offset]);
}
public function offsetGet($offset) {
return isset($this->enumerableArray[$offset]) ? $this->enumerableArray[$offset] : null;
}
public function count() {
return count($this->enumerableArray);
}
}
?>
“Подмешаем” модуль к Record и добавим методы к которым обращается Enumerable
public $name = "Record";
protected $enumerableArray = array();
public function __construct() {
$this->addModule("Finder");
$this->addModule("Validator");
$this->addModule("Enumerator");
}
public function getEnumerableArray() {
return $this->enumerableArray;
}
}
К сожалению, ниже описанный код вызовет ошибку. Итератор тоже не работает.
$rec['key'] = "Value";
Но не смотря на такие досадные ограничения, которые я не нашел как обойти, этот способ можно с успехом применять на практике. Хотя бы для того, чтобы просто разбивать тяжелые классы, создавая подобие фасада.
Если я где-то ошибся, надеюсь, умные люди меня поправят.

(4 голосов, средний: 4.75 из 5)
Интересная идея реализации mixin’ов. Возьму на заметку.
Sergey Kuznetsov
17 Фев 10 at 20:34
Спасибо. Я рад, что заметка оказалась кому-то интересна.
undr
17 Фев 10 at 21:30