Однажды вечером, чтобы реализовать поведение ORM в моем велосипедном фреймворке, мне понадобилось что-то, что вело бы себя как примесь в Ruby или как метод расширения в C# (или как черта/прививка в будущих версиях PHP).
Просто ради развлечения я решил посмотреть как я могу реализовать миксины в PHP. Если вы не знаете, что такое примесь, не беда, я вам сейчас все расскажу.
Я приглашаю вас присоединиться к моему обсуждению реализации примесей в PHP и программирования небольшой библиотеки, которая позволит вам их реализовать.
Статья ориентирована на PHP-разработчиков начального и среднего уровня (главное, чтобы вы хорошо разбирались в ООП).
В процессе я также допущу небольшую ошибку относительно тонкостей работы PHP 5.3 с классами, через некоторое время укажу на нее и предложу исправить.
Я также предложу свое решение для вашей критики.
Наслаждайся чтением.
Что такое примесь? Миксин — это класс, который просто предоставляет свои методы и свойства другим классам.
Можно предположить, что смешивание других классов в класс — это просто вариант эмуляции множественного наследования, который не реализован в PHP. Для наглядности приведу небольшой пример на псевдокоде, по синтаксису схожий с PHP:
<Эphp mixin Timeable { private $timeStarted; private $timeStopped; public function start() { $timeStarted = time(); } public function stop() { $timeStopped = time(); } public function getElapsed() { return $timeStopped - $timeStarted } } mixin Dumpable { public function dump() { var_dump($this); } } class MyClass mixing Timeable, Dumpable { public function hello() {} } $a = new MyClass(); $a->start(); sleep(250); $a->stop(); echo $a->getElapsed(); $a->dump(); ?>Идея ясна? Миксины просто добавляют к классу свою функциональность, как если бы класс был унаследован от всех них одновременно.
В то же время они могут манипулировать членами класса, в котором они смешаны.
Эту функциональность мы реализуем в PHP. Давайте поставим перед собой задачу.
- Нам нужно реализовать возможность смешивать функциональность указанных классов-примесей с экземплярами данных классов.
- Классы примесей не следует загружать перед классом, в который они подмешиваются.
В приведенном выше примере используется псевдосинтаксис, который позволяет нам определять классы примесей непосредственно в объявлении класса.
Но у этого метода есть свои недостатки.
Что делать, если во время работы программы нам нужно добавить к классам нашей системы плагины, которые будут действовать как примеси? В этом случае мы могли бы объявить все миксины где-нибудь в скрипте инициализации, и нам важно, чтобы такое объявление не приводило к загрузке классов.
- Если миксин примешан к классу, это означает, что его функционал должен быть доступен и в классе-потомке этого класса.
Все-таки мы используем объектно-ориентированный язык и это будет логично.
- При реализации желательно учитывать, что использование членов классов-миксинов не должно быть очень медленным, особенно если в системе будет использоваться много миксинов.
- Изменение существующих классов для использования примесей не должно требовать перепроектирования существующей системы.
Как следствие, это означает, что должен быть другой способ, помимо наследования от абстрактного класса, научить класс смешивать функциональность других классов.
- Публичные свойства и методы миксинов должны быть доступны через экземпляр хост-класса (в дальнейшем я буду называть его «агрегатором», поскольку он может агрегировать внутри себя несколько миксинов).
А частные и защищенные должны быть видны только самой примеси.
- Миксин должен иметь возможность доступа даже к скрытым и защищенным полям своего класса-агрегатора (при установке этого требования я руководствовался Ruby, у которого нет скрытых и защищенных свойств в том смысле, как они есть в C++, PHP или C#.
Там к нему можно получить доступ откуда угодно, можно применить к любым полям класса (но поскольку миксин может добавлять новое поведение, ему может потребоваться защищенная информация от класса-агрегатора).
Давайте подумаем об этом.
Возможно, мы захотим добавить разные примеси к разным классам системы.
То есть где-то мы должны хранить информацию о том, какие классы с какими примесями смешаны.
Такая информация по проекту является глобальной и должна быть доступна отовсюду.
Поэтому для реализации такого хранилища я выбрал статический класс (В PHP нет статических классов в том виде, в котором они существуют в C#.
Под статическим классом я подразумеваю класс, экземпляр которого не нужно создавать.
Вся его функциональность будет быть реализовано с помощью статических методов, доступных через класс имени).
В качестве небольшой задачи предлагаю (если вам интересно; после того, как вы дочитаете статью до конца) перепроектировать реестр, чтобы не требовалось использование синглтона.
Из вышесказанного следует, что в реестре должна быть возможность регистрировать миксины для классов-агрегаторов.
И чуть выше мы говорили, что если мы регистрируем миксин для класса, то функционал этого миксина должен быть подмешан во все классы-потомки.
Мы не можем получить список классов-предков сразу при регистрации (ведь нам нужно избегать загрузки классов, а проверка иерархии классов потребует этого).
Из этого следует, что список соответствий (класс => список примесей) мы построим позже, когда это действительно понадобится.
Кроме того, такой список нужно будет кэшировать, чтобы при создании новых экземпляров классов-агрегаторов он не перестраивался.
Time native: 0.57831501960754 Time byname: 1.5227220058441 Time mixed: 7.5425450801849 Time reflection: 12.221807956696Функция регистрации оказалась довольно простой.
Передаем ему имя класса-агрегатора и список миксинов для него.
Для удобства список примесей можно указывать через запятую.
func_get_args() позаботится об этом (добавьте отличную поддержку для указания списка примесей в виде массива, если интересно).
Затем мы просто добавляем каждый миксин в список миксинов этого класса.
И последний вызов в конце функции очищает кеш, так как регистрация миксина для этого класса также добавит его ко всем его потомкам, что потребует пересборки кеша.
Теперь давайте напишем функцию кэширования.
Он должен пройтись по списку зарегистрированных для них классов и миксинов и добавить в него все классы-потомки этого с таким же списком миксинов.
Результатом является кэш.
Для функции кэширования нам нужна функция, которая получает список предков данного класса: class Registry {
private static $registeredMixins = array();
public static function register($className, $mixinClassName) {
$mixinClassNames = func_get_args();
unset($mixinClassNames[0]);
foreach ($mixinClassNames as $mixinClassName) {
self::$registeredMixins[$className][] = $mixinClassName;
}
self::$classNameToMixinCache = array();
}
}
Обратите внимание, что функция добавляет в кеш список миксинов только для данного класса.
Мы не будем создавать весь кеш сразу, так как большая часть его содержимого может никогда не понадобиться.
Прежде чем создавать кэш, мы проверили, делали ли мы это раньше.
Теперь, если нам нужно получить список миксинов для данного класса, мы можем использовать эту функцию: private static $classNameToMixinCache = array();
private static function getAncestors($className) {
$classes = array($className);
while (($className = get_parent_class($className)) !== false) {
$classes[] = $className;
}
return $classes;
}
private static function precacheMixinListForClass($className) {
if (isset(self::$classNameToMixinCache[$className])) {
return;
}
$ancestors = self::getAncestors($className);
$result = array();
foreach ($ancestors as $ancestor) {
if (isset(self::$registeredMixins[$ancestor])) {
$result = array_merge($result, self::$registeredMixins[$ancestor]);
}
}
self::$classNameToMixinCache[$className] = array_unique($result);
}
Перейдем к следующему шагу.
Представьте, что мы вызываем метод класса-агрегатора, который определен не в нем, а в одном из миксинов.
Что нам нужно сделать? Нам нужно получить список примесей для этого класса, затем просмотреть их и посмотреть, определяет ли какой-нибудь из них нужный нам метод.
Поскольку миксин — это класс, мы делаем что-то вроде этого: public static function getMixinsFor($className) {
self::precacheMixinListForClass($className);
return self::$classNameToMixinCache[$className];
}
То есть, если в кеше уже есть запись для данного класса и имени метода, мы просто их возвращаем.
Если нет, то получаем из нашего кеша список миксинов для этого класса, просматриваем их и проверяем, реализует ли какой-нибудь из них нужный нам метод. Если да, добавьте его в кеш и верните имя миксина.
Если мы ничего не смогли найти, мы генерируем исключение.
Точно аналогичный вариант получается и для свойств.
Предлагаю вам написать это самостоятельно.
Вот и все.
Мы внедрили реестр.
Перейдем к программированию класса миксина.
Программирование миксина.
Итак, нечистота.
Что такое примесь? Миксин — это обычный класс.
Просто он умеет работать с полями другого класса.
И было бы логично передать ему в конструкторе экземпляр этого другого класса.
private static $methodLookupCache = array();
public static function getMixinNameByMethodName($className, $methodName) {
if (isset(self::$methodLookupCache[$className][$methodName])) {
return self::$methodLookupCache[$className][$methodName];
}
self::precacheMixinListForClass($className);
foreach (self::$classNameToMixinCache[$className] as $mixin) {
if (method_exists($mixin, $methodName)) {
self::$methodLookupCache[$className][$methodName] = $mixin;
return $mixin;
}
}
throw new MemberNotFoundException("$className has no mixed method $methodName()!");
}
Я назвал базовый класс миксина Base просто потому, что в моем проекте он принадлежит пространству имен Mixins и нет необходимости называть его более конкретно.
Но вы можете называть это как угодно.
Мы можем работать с публичными полями и методами напрямую через переменную owningClassInstance. А вот со скрытыми и защищенными придется работать через размышление.
Ничего сложного.
Вот все определения функций: class Base {
protected $_owningClassInstance;
protected $_owningClassName;
public function __construct($owningClassInstance) {
$this->_owningClassInstance = $owningClassInstance;
$this->_owningClassName = get_class($owningClassInstance);
}
}
Обратите внимание, что здесь я снова использовал кэширование, чтобы мне не приходилось постоянно создавать и настраивать экземпляры системных классов для работы отражения.
Чтобы уменьшить потребление памяти, при необходимости от кэширования можно отказаться.
Возможно, кто-то уже заметил, что функции Method_exists() и property_exists(), которые мы использовали в классе реестра, проверяют наличие в миксине скрытых и защищенных функций с заданным именем, наряду с публичными.
Это приводит к тому, что класс-агрегатор будет «пытаться» вызвать одноименную функцию, если она определена как скрытая или защищенная.
В результате мы все равно получим ошибку, но я предпочитаю делать это явно: protected $_owningPropertyReflectionCache;
protected $_owningMethodReflectionCache;
protected function getProtected($name) {
if (! isset($this->_owningPropertyReflectionCache[$name])) {
$property = new \ReflectionProperty($this->_owningClassName, $name);
$property->setAccessible(true);
$this->_owningPropertyReflectionCache[$name] = $property;
}
return $this->_owningPropertyReflectionCache[$name]->getValue($this->_owningClassInstance);
}
protected function setProtected($name, $value) {
if (! isset($this->_owningPropertyReflectionCache[$name])) {
$property = new \ReflectionProperty($this->_owningClassName, $name);
$property->setAccessible(true);
$this->_owningPropertyReflectionCache[$name] = $property;
}
$this->_owningPropertyReflectionCache[$name]->setValue($this->_owningClassInstance, $value);
}
protected function invokeProtected($name, $parameters) {
$method = new \ReflectionMethod($this->_owningClassName, $name);
$method->setAccessible(true);
$parameters = func_get_args();
unset($parameters[0]);
$method->invokeArgs($this->_owningClassInstance, $parameters);
}
В качестве небольшой задачи попробуйте исправить неправильное поведение нашего класса реестра.
Более того, это сделает невозможным вызов общедоступного метода примеси с именем, которое ранее уже было обнаружено как скрытое или защищенное в другом.
Хм.
Вот и все.
Смесь готова к использованию.
Остался последний шаг — реализация платформы для смешивания примесей — классов-агрегаторов.
Вот что мы сейчас сделаем.
Пишем класс-агрегатор.
Что может наш класс-агрегатор? Он может хранить экземпляры классов-примесей и вызывать их методы.
Что ж, обратимся к свойствам.
Мы реализуем это поведение с помощью «волшебных» методов PHP. public function __call($name, array $arguments) {
throw new MemberNotFoundException(
"Method $name is not defined or is not accessible in mixin \"" .
get_class() .
"\""); } public function __get($name) { throw new MemberNotFoundException( "Property $name is not defined or is not accessible in mixin \"" .
get_class() .
"\""); } public function __set($name, $value) { throw new MemberNotFoundException( "Property $name is not defined or is not accessible in mixin \"" .
get_class() .
"\"");
}
В коде конструктора мы просто получаем список примесей для класса, затем просматриваем их и создаем их экземпляры.
Переменная $aggregatorClassInstance используется для того, чтобы нам не приходилось наследовать наш класс от класса Aggregator. Мы сможем включить класс Aggregator в другой класс и вызвать его конструктор с параметром $aggregatorClassInstance, равным экземпляру этого другого класса.
Соответственно, в этом случае мы получим список миксинов для этого класса-владельца и передадим в миксины соответствующий экземпляр класса-агрегатора.
Если приведенное выше объяснение показалось вам слишком сложным, не беда.
Прокрутите немного вниз, там есть примеры.
Посмотрите, чем пример «Наследование» отличается от примера «Композиция» и как они работают. Мы реализуем «волшебные методы».
class Aggregator {
protected $_mixins;
protected $_className;
public function __construct($aggregatorClassInstance = false) {
$this->_className = $aggregatorClassInstance ? get_class($aggregatorClassInstance) : get_class($this);
$mixinNames = Registry::getMixinsFor($this->_className);
foreach ($mixinNames as $mixinName) {
$this->_mixins[$mixinName] = new $mixinName($aggregatorClassInstance ? $aggregatorClassInstance : $this);
}
}
}
Каждый из магических методов обращается к реестру за информацией.
Это просто.
Используемый нами класс исключений выглядит следующим образом: public function __call($name, array $arguments) {
return call_user_func_array(array($this->_mixins[Registry::getMixinNameByMethodName($this->_className, $name)], $name), $arguments);
}
public function __get($name) {
return $this->_mixins[Registry::getMixinNameByPropertyName($this->_className, $name)]->$name;
}
public function __set($name, $value) {
$this->_mixins[Registry::getMixinNameByPropertyName($this->_className, $name)]->$name = $value;
}
public function __isset($name) {
return isset($this->_mixins[Registry::getMixinNameByPropertyName($this->_className, $name)]->$name);
}
Давайте посмотрим на несколько примеров
Для начала давайте посмотрим на традиционную схему наследования: class MemberNotFoundException extends \Exception {}
Теперь взгляните на принципиальную схему: class MixinAggregatorSample extends Mixins\Aggregator {
}
class MixinHello extends Mixins\Base {
protected $inaccessible;
public $text = "I am a text!\r\n";
public function hello() {
echo ("Hello from mixin!\r\n");
}
}
Mixins\Registry::register("MixinAggregatorSample", "MixinHello");
$a = new MixinAggregatorSample();
$a->hello(); //Accesing mixed methid
echo ($a->text); //Accessing mixed property
$a->text = "I am also a text!\r\n"; //Setting mixed property
//$a->inaccessible = 'Error here'; //Throws exception
//$a->inaccessible2 = 'Error here'; //Throws yet another exception (Homework: explain, why)
echo ($a->text);
var_dump(isset($a->text));
Вы видите разницу? В случае включения мы вольны наследовать наш класс-агрегатор от любого другого без потери функциональности.
Конечно, чтобы его нормально использовать, вам придется реализовать все магические методы, а не только __call().
Производительность Я сделал некоторые замеры производительности полученной библиотеки.
Замеры очень приблизительные, проводились на домашнем компьютере с открытой IDE, Winamp и всем, что требуется.
class MixinAggregatorSample {
protected $_aggregator;
public function __construct() {
$this->_aggregator = new Mixins\Aggregator($this);
}
public function __call($name, $arguments) {
return $this->_aggregator->__call($name, $arguments);
}
}
class MixinHello extends Mixins\Base {
public function hello() {
echo ("Hellp from mixin!");
}
}
Mixins\Registry::register("MixinAggregatorSample", "MixinHello");
$a = new MixinAggregatorSample();
$a->hello();
- Native – время для прямого вызова метода класса в PHP.
- Byname - время вызова метода класса через имя $myClass-> $methodName
- Mixed – время вызова смешанного метода
- Отражение — время вызова смешанного метода, который изменяет свойство класса посредством отражения.
Те.
= смешанный + отражение.
- Время указано в секундах на 800 000 вызовов.
Как правило, миксиновые методы не вызываются в скрипте тысячи раз, и 10 микросекунд на вызов метода против 0,7 микросекунды для нативных методов — вполне приемлемый вариант. Особенно если учесть, что время, затрачиваемое на htmlspecialchars(), например, на большой объем текста или на выполнение запроса к базе данных, значительно выше.
Поскольку мы практически везде используем кеширование на основе хеш-массивов PHP, то с увеличением количества миксинов и классов-агрегаторов производительность не должна сильно падать.
Однако если кто-то сделает необходимые анализы, я буду очень рад. Эпилог Буду рад услышать вашу критику данной статьи.
Меня особенно интересует, удалось ли мне сделать материал понятным всем читателям.
Конечно, данная статья не претендует на полноту, точность и безошибочность.
Буду очень благодарен, если вы меня поправите.
Выкладываю код полученной библиотеки здесь .
Конечно, это учебный проект и прежде чем использовать его в реальном проекте следует все хорошенько подумать и протестировать.
Есть некоторые проблемные моменты, например, наличие нескольких миксинов с одинаковыми именами публичных методов в одном классе.
Если тема миксинов в PHP вас интересует, предлагаю вам также зайти через Google. Теги: #php #Mixin #OOP #php
-
Зеллигер, Гюго
19 Oct, 24 -
Предскажи Будущее, Хабраман
19 Oct, 24 -
Пользовательский Интерфейс В Стиле Комильфо
19 Oct, 24 -
Я Хочу Такую Картинку
19 Oct, 24 -
Видео@Mail.ru Начало Работать С Hd
19 Oct, 24