Undr

На память

Использование ruby-like модулей в PHP

1 Star2 Stars3 Stars4 Stars5 Stars (4 голосов, средний: 4.75 из 5)
Loading ... Loading ...

with 2 comments

Недавно мне пришлось работать с очень длинными классами, примерно 1000-1300 строк. Они, знаете ли, не очень удобны для работы. Все время хотелось их разнести по разным модулям, как в ruby, чтобы не мешались в одной куче. А потом подключать эти модули в класс, возможно даже не в один. В итоге я сделал следующее:

Я создал два класса Modularize и Module. От первого я унаследовал классы, которые будут состоять из модулей. От второго – классы-модули, которые я буду подмешивать в классы, потомки Modularize.

  <?php
  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 со множеством методов, получаем такой:

  <?php
  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.

  <?php
  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 код будет выглядеть так:

  <?php
  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";
    }
  }
  ?>

Для проверки написал такой код

  <?php
  $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 и подмешать его в требуемые классы. Остается добавить в эти классы методы, позволяющие получить и установить данные объекта в виде массива.

  <?php
  class JSONize extends Module {
   
    public function toJson() {
        return json_encode($this->parent->toArray());
    }
   
    public function fromJson($json) {
        $this->parent->fromArray(json_decode($json, true));
    }
  }
  ?>
  <?php
  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;
    }
  }
  ?>
  <?php
  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];
          }
        }
    }
  }
  ?>

Для проверки:

  <?php
  $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, и “подмешивать” его к разным классам. Но к сожалению, так просто это не получится. Например есть классы:

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

  class Record extends Modularize {
    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 = new Record();
  $rec['key'] = "Value";

Но не смотря на такие досадные ограничения, которые я не нашел как обойти, этот способ можно с успехом применять на практике. Хотя бы для того, чтобы просто разбивать тяжелые классы, создавая подобие фасада.

Если я где-то ошибся, надеюсь, умные люди меня поправят.

Написал undr ()

17 февраля 2010 в 17:07

Размещено в Примеры, Программирование

Метки: ,

2 Responses to 'Использование ruby-like модулей в PHP'

Подписаться на комментарии or TrackBack to 'Использование ruby-like модулей в PHP'.

  1. Интересная идея реализации mixin’ов. Возьму на заметку.

    Sergey Kuznetsov

    17 Фев 10 at 20:34

  2. Спасибо. Я рад, что заметка оказалась кому-то интересна.

    undr

    17 Фев 10 at 21:30

Оставьте комментарий