В PHP 5.4 планируются так называемые трейты (traits) - возможность создавать классы как комбинацию методов и полей других классов. По сути, это реализация множественного наследования в PHP.
Однако, проблема в том, что это будет только в PHP 5.4, а даже 5.3 не везде установлен. А система является потенциально очень удобной. И хочется использовать уже сейчас. Я решил сделать эмуляцию трейтов, работающую в 5.х. И это получилось! Я расскажу как.
(Система находится в разработке. Это только первая, предварительная версия).
Итак, давайте посмотрим, что мы хотим от трейтов и чего можем добиться в PHP 5. Для этого рассмотрим наиболее вероятный случай их использования. Случай следующий: мы берем класс-наследник (Н) и хотим в нем использовать методы из нескольких заранее готовых кусочков (К1, К2 и т. п.).
Сразу можно сказать, что мы должны явно определить "унаследованные методы" в Н. Иначе мы не получим pre Insight. Но, конечно, мы не должны указывать содержимое этих методов - они должны автоматически подхватиться системой. Это выглядит, например, так:
< ?php class trait1 extends base_simpletrait { protected $var; protected $traits_before = array("trait2"); protected $traits_after = array("trait2", "trait3"); public function __construct() { parent::__construct(); $this->method(); } protected function method() { $this->trait_before(); echo "var: " . $this->var . " n"; echo "trait: " . get_class($this) . " n"; $this->var = "t1"; echo "var: " . $this->var . " n"; $this->trait_after(); echo "var: " . $this->var . " n"; } } class trait2 extends base_simpletrait { protected $var; protected function method() { echo "trait: " . get_class($this) . " n"; $this->var = "t2"; } } class trait3 extends base_simpletrait { protected $var; protected function method() { echo "trait: " . get_class($this) . " n"; $this->var = "t3"; } } $trait = new trait1(); ?>
(Здесь trait1 - Н, trait2 и trait3 - К1, К2).
Как вы видите, моя реализация похожа как на "трейты", так и на подход аспектно-ориентированного программирования, или на систему плагинов, или на EventListeners в Java. То есть, метод Н не заменяется на метод одного К, как в PHP 5.4, вместо этого на него навешиваются сколько угодно аналогичных методов К, выполняемых как до, так и после метода Н. Сам метод Н может не содержать никакого кода кроме обращения к К. Кстати, методы trait_before и trait_after умеют возвращать возвращаемое значение методов К. Если было выполнено несколько К, возвращается значение последнего.
Итогом выполнения будет:
trait: trait2 var: t2 trait: trait1 var: t1 trait: trait2 trait: trait3 var: t3
Как видим, были выполнены методы method во всех К в нужном порядке. Также внутри К было изменено значение поля var. Все трейты действительно используются как единый объект (причем интересно, что один и тот же К может подключаться к Н несколько раз и в before, и в after).
Теперь давайте поймем - а как это сработало? Основные вопросы:
1. Как мы поняли какой метод вызвал трейты и какие параметры были переданы в этот метод (в данном примере параметров не было, но если в оригинальный метод они передадутся, то передадутся и во все методы К)?
2. Как мы обратились к protected методу чужого класса?
3. Как мы из разных классов изменяли protected поле var в одном из них?
Начнем с последнего вопроса. Дело в том, что мы... не меняем одно поле в разных классах! Система работает иначе - она определяет общие поля в разных классах и перед вызовам К посылает туда значения полей Н, а после вызова метода Н посылаем изменившиеся переменные обратно. То есть, алгоритм таков:
К.var = Н.var; К.method(); // меняет var Н.var = К.var;
То есть, вместо общих полей используется их синхронизация.
Естественно, для этого мы должны иметь доступ к чужим полям и методам (вопрос 2). Для этого в base_simpletrait существуют protected методы trait_get, trait_set и trait_call. Они могут вызываться другими трейтами не будучи public, потому что в PHP объекты одного класса имеют доступ ко всем методам друг друга. При этом к методу method в наследниках мы извне не обратимся - т. к. он определен в разных классах-наследниках base_simpletrait. А get, set и call уже внутри себя спокойно вызывают методы и меняют поля наследников, к которым у них есть доступ. Естественно, это не позволяет объединять private поля и методы, но это как раз очень логично.
Теперь последний вопрос №1. Как мы их trait_before/trait_after понимаем, какой метод с какими параметрами вызывать у К? Очень легко. Для этой цели используется PHP функция debug_backtrace. Она выдает массив всего стека вызовов функций друг друга, в том числе имена функций и переданные в них параметры. Нам достаточно только узнать имя и параметры предыдущего метода - и мы сможем продублировать его вызов в К.
Собственно, всё. Ниже выкладываю код базового trait-класса. Пользуйтесь! (и напоминаю, что это лишь предварительная, но рабочая версия):
< ?php /** * @author DileSoft */ abstract class base_simpletrait { protected $traits_before = array(); protected $traits_after = array(); protected $trait_before_objects = array(); protected $trait_after_objects = array(); public function __construct() { $this->trait_init(); } protected function trait_init() { foreach ($this->traits_before as $key => $trait_name) { $this->trait_before_objects[$key] = new $trait_name(); } foreach ($this->traits_after as $key => $trait_name) { $this->trait_after_objects[$key] = new $trait_name(); } } protected function trait_before() { $backtrace = debug_backtrace(); foreach ($this->traits_before as $key => $trait_name) { $result = $this->trait_run_method($this->trait_before_objects[$key], $backtrace[1]["function"], $backtrace[1]["args"]); } return $result; } protected function trait_after() { $backtrace = debug_backtrace(); foreach ($this->traits_after as $key => $trait_name) { $result = $this->trait_run_method($this->trait_after_objects[$key], $backtrace[1]["function"], $backtrace[1]["args"]); } return $result; } protected function trait_run_method(base_simpletrait $trait, $name, $args) { return $trait->trait_call($this, $name, $args); } protected function trait_call(base_simpletrait $trait_from, $name, $args) { if (method_exists($this, $name)) { $this->sync_from($trait_from); $result = call_user_func_array(array($this, $name), $args); $this->sync_to($trait_from); return $result; } } protected function trait_get($name) { return array_key_exists($name, get_class_vars(get_class($this))) ? $this->$name : null; } protected function trait_set($name, $value) { if (array_key_exists($name, get_class_vars(get_class($this)))) { $this->$name = $value; } } protected function sync_from(base_simpletrait $trait) { $class_vars = get_class_vars(get_class($this)); foreach ($class_vars as $name => $default) { if (array_key_exists($name, get_class_vars("base_simpletrait"))) { continue; } $this->$name = $trait->trait_get($name); } } protected function sync_to(base_simpletrait $trait) { $class_vars = get_class_vars(get_class($this)); foreach ($class_vars as $name => $default) { if (array_key_exists($name, get_class_vars("base_simpletrait"))) { continue; } $trait->trait_set($name, $this->$name); } } } ?>
freehabr
Постоянные ссылки
При копировании ссылка на TeaM RSN обязательна!