В 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 обязательна!

URI

Html (ЖЖ)

BB-код (Для форумов)