
Mixins in PHP
Итак, вдруг нам ни с того, ни с сего, но потребовалось использовать примеси в php (вот такие мы ортодоксы). Динамические языки типа Python или Ruby делают это на уровне самого языка с присущей им легкостью и изящностью. PHP этим не обладает. Однако, нашелся способ решения задачи.
Точнее, делали это и раньше. Гуглом находятся некоторые решения . Даже легендарный whytheluckystiff в каком-то мохнатом году использовал php и пытался добиться от него динамики . Но все эти решения используют eval(). Терпеть этого не могу.
Для того, чтобы отказаться от eval() используем магические методы типа __call() . Два объекта — основной, и примесь. Основной объект хранит ссылки на экземпляры всех примесей. Примеси определяют методы, отсутствующие в основном объекте. В магическом методе __call() основного объекта в цикле по всем примесям определяем наличие в них требуемого метода. Если метод найден — передаем управление ему, если ни в одной из примесей метода нет — что ж,... эксепшен!
Далее. Примесь должна знать об основном объекте, и должна уметь исполнять его методы. То есть, как минимум ссылка на родительский объект должна быть, а родительский объект должен предоставлять доступ к своим свойствам и методам, к которым примесь обращается через те же магические методы.
Итак, определим два абстрактных класса — примесь (Mixin) и родительский объект (Caller):
private $clr;
public function __construct($caller){
$this->clr = $caller;
}
public function __get($prop){
return $this->clr->__access_get_prop($prop);
}
public function __set($prop, $value){
return $this->clr->__access_set_prop($prop, $value);
}
public function __call($method, $value){
return $this->clr->__access_call($method, $value);
}
}
abstract class Caller{
protected $mixins = array(); // массив экземпляров примсей
protected function __call($method, $value){
$found = false;
foreach($this->mixins as $cname=>$co){
if(method_exists($co, $method)){
return $co->$method();
$found = true;
}
}
if(!$found){
throw new Exception("call to unfdefined method ".$method);
}
}
public function __access_get_prop($prop){
if(property_exists($this, $prop)){
return $this->$prop;
}
}
public function __access_set_prop($prop, $value){
if(property_exists($this, $prop)){
return $this->$prop = $value;
}
}
public function __access_call($method, $value){
if(method_exists($this, $method)){
return call_user_func_array(array($this, $method), $value);
}
}
}
/*
* использование
*/
// конкретная примесь с присущим ей функционалом
class SayHello extends Mixin{
public function say(){
return "Hello ".$this->name;
}
public function sayLoud(){
return 'Hello '.$this->loud().'!!!';
}
public function addPrefix(){
$this->name = "Mr. ".$this->name;
}
}
// конкретный родительский класс, расширенный примесью
class Person extends Caller {
public $name;
function __construct($name){
$this->name = $name;
// тут создаем экземпляр(ы) примеси(ей)
$this->mixins[] = new SayHello($this);
}
public function loud(){
return '<b>'.$this->name.'</b>';
}
public function saySomething(){
$this->addPrefix();
return '<h2>'.$this->name.'</h2>';
}
}
$p = new Person("Schleicher");
echo $p->say(); // Hello Schleicher
echo $p->sayLoud(); // Hello <b>Schleicher</b>!!!
echo $p->saySomething(); // <h2>Mr. Schleicher</h2>
То есть, видно, что в данном случае мы прозрачно обращаемся к методам родительского класса из примеси, и спокойно вызываем методы примесей как собственные методы родительского класса. Что и требовалось в общем-то...
Достоинства
- Имеем рабочую схему Mixins без использования eval()
Недостатки:
- все равно это дыра в производительности, но ей мы всегда жертвуем в угоду более простой разработки.
- Все методы примеси должны быть public, хотя по логике это не всегда так.
- каждый родительский класс должен создать экземпляры всех своих примесей. Впрочем, он может использовать общие для всех экземпляры, получая их с помощью Registry
- Может быть есть что-то еще, необходимо всестороннее тестирование.
С помощью этой схемы работают комментарии на этом сайте. В данном случае, родителскими классами выступают модули (текстовый — на текстовых страницах разделов Outdoot и Web, модуль событий — в разделе блога). Все они используют в качестве примеси компонент комментариев и приобретают его функциональность. Разумеется, функциональность комментариев можно было вынести в класс-предок и текстов и новостей, некий абстрактный модуль, но это конкретные детали. Целесообразность применения примесей не является темой этой статьи.