- PVSM.RU - https://www.pvsm.ru -
При разработке крупных проектов довольно часто возникает ситуация, когда сквозной функционал, слабо относящийся к бизнес-логике, сильно раздувается, заполняя код однотипными конструкциями. Это может быть логирование операций, работа с кешем или проверка прав доступа. Тут нам на помощь приходит АОП [1].
Для PHP существует несколько реализаций этой парадигмы программирования. К сожалению, среди них мне не удалось найти решение, которое бы можно было легко внедрить в уже существующий большой проект и удовлетворяющее эстетическим требованиям к коду.
Самое простое решение — использование «волшебных методов [2]» __call и __callStatic. Эти методы вызываются (если они определены в классе) при обращении к несуществующему методу класса. В качестве аргументов они получают имя несуществующего метода и переданные ему параметры.
В данном случае, приложение строится таким образом, что реальные методы имеют имя отличное от имени указанном в вызывающих их конструкциях. Сквозной функционал реализуется в «волшебных методах», которые, при необходимости, передают управление реальным методам классов.
Этот способ подразумевает наличие посредника, позволяющего использовать «синтаксический сахар». Необходимый функционал описывается вспомогательным синтаксисом (xml/json конфигурация, дополнительные php-классы или аннотации в коде), который разбирается посредником. На основе разбора генерируется результирующий код, который содержит вставки сквозного функционала в необходимые места.
Небезызвестное расширение runkit [3] позволяет изменять код скрипта во время его выполнения. На его основе я разработал небольшую библиотеку, которая позволяет довольно просто решить поставленную задачу.
Встречайте: Annotator [4].
Библиотека реализует 4 типа обработчиков:
Обработчик типа Info получает информацию о методе во время обработки класса. Это позволяет «зарегистрировать» метод для дальнейшего использования в приложении. Например с его помощью можно назначить метод на обработку определённого URL и таким образом реализовать роутинг в приложении.
Обработчик типа Before выполняется до, вызываемого метода. Он получает всю необходимую информацию о вызываемом методе включая входные параметры, которые можно изменить до передачи управления в вызываемый метод.
Обработчик типа After выполняется после вызываемого метода. Кроме информации о методе и его параметрах, он также получает результат выполнения вызываемого метода, который, при необходимости, можно заменить.
Обработчик типа Around выполняется вместо вызываемого метода. Внутри обработчика есть возможность ручной передачи управления в вызываемый метод, если это необходимо.
Для работы Annotator требуется PHP 5.4 и модуль runkit.
Последняя версия в официальном репозитории [5] runkit, ещё не адаптирована для PHP 5.4, но, к счастью, его уже адаптировал другой разработчик [6].
phpize && ./configure && make && sudo make install
Класс предоставляет 4 заранее зарезервированных аннотации для всех типов обработчиков, при этом есть возможность зарегистрировать свои аннотации любого из 4 типов.
<?php
require_once __DIR__ . '/Annotator.php';
class Advice {
public static function infoStatic($point, $options) {
var_dump($point);
var_dump($options);
}
}
class Test {
/**
* @info Advice::infoStatic hello world
*/
public static function testInfoStatic() {
return 'info';
}
}
Annotator::compile('Test');
Уже во время вызова Annotator::compile('Test'); будет вызван обработчик infoStatic класса Advice, который получит инфрмацию о методе testInfoStatic класса Test и параметры обработчика в качестве массива array('hello', 'world').
В этом примере, мы зарегистрируем свою аннотацию вместо использования стандартной, а в качестве обработчика используем метод объекта вместо статического метода класса.
<?php
require_once __DIR__ . '/Annotator.php';
class Advice {
public function before($point, $params, $options) {
$params['string'] = 'bar';
}
}
class Test {
/**
* @registered_before
*/
public function testBefore($string) {
return $string;
}
}
$advice = new Advice();
Annotator::register('registered_before', array($advice, 'before'), Annotator::BEFORE);
Annotator::compile('Test');
$test = new Test();
echo $test->testBefore('foo');
Методом Annotator::register мы создали аннотацию @registered_before, ассоциированную с методом before объекта $advice. При вызове testBefore управление будет передано в обработчик, который заменит параметр $string и вместо ожидаемого «foo» в результате работы скрипта будет выведено «bar».
<?php
require_once __DIR__ . '/Annotator.php';
class Advice {
public function power($point, $params, $options, $result) {
return pow($result, $options[0]);
}
}
class Test {
/**
* @power 4
*/
public function testAfter($number) {
return $number + 1;
}
}
$advice = new Advice();
Annotator::register('power', array($advice, 'power'), Annotator::AFTER);
Annotator::compile('Test');
$test = new Test();
echo $test->testAfter(1);
В этом примере результат работы метода testAfter будет возведён в степень 4. Скрипт выведет значение 16.
<?php
require_once __DIR__ . '/Annotator.php';
class Advice {
private static $cache = array();
public function cache($point, $params, $options, $proceed) {
if (isset(self::$cache[$options[0]])) {
// Если значение ключа содержится в кеше - возвращаем его
return self::$cache[$options[0]];
} else {
// Если значения в кеше нет - выполняем функцию
$result = $proceed();
// Перед возвращением значения не забываем положить его в кеш
self::$cache[$options[0]] = $result;
return $result;
}
}
}
class Test {
/**
* @cache around_cache_key
*/
public function testAround($string) {
return $string;
}
}
$advice = new Advice();
Annotator::register('cache', array($advice, 'cache'), Annotator::AROUND);
Annotator::compile('Test');
$test = new Test();
echo $test->testAround('foo') . PHP_EOL;
echo $test->testAround('foo') . PHP_EOL;
echo $test->testAround('foo') . PHP_EOL;
Этот пример представляет реализацию простого механизма кеширования. Метод testAround вызывается 3 раза подряд, но будет выполнен только 1 раз. Остальные 2 раза значение будет взято из статической переменной $cache класса Advice, куда оно будет сохранено после первого вызова.
Annotator позволяет навешивать несколько обработчиков, в том числе и разных типов, на один метод.
<?php
require_once __DIR__ . '/Annotator.php';
class Advice {
public static function before1($point, $params, $options) {
$params['string'] .= 'before1';
}
public static function before2($point, $params, $options) {
$params['string'] .= ' before1';
}
public static function after1($point, $params, $options, $result) {
return $result . ' after1';
}
public static function after2($point, $params, $options, $result) {
return $result .= ' after2';
}
public static function around1($point, $params, $options, $proceed) {
return $proceed() . ' around1';
}
public static function around2($point, $params, $options, $proceed) {
return $proceed() . ' around2';
}
}
class Test {
/**
* @before Advice::before1
* @after Advice::after1
* @around Advice::around1
* @before Advice::before2
* @after Advice::after2
* @around Advice::around2
*/
public function testMulti($string) {
return $string;
}
}
Annotator::compile('Test');
$test = new Test();
echo $test->testMulti('');
В результате работы этого примера будет выведена строка «before1 before1 around1 around2 after1 after2».
Каждый тип обработчика имеет свой набор параметров:
На основе Annotator можно очень просто и быстро реализовать удобные механизмы, позволяющие сильно сократить код и улучшить структуру приложения, упрощая его поддержку. Например, кроме реализации АОП с его помощью можно довольно легко реализовать паттерн Dependency injection [9].
Необходимо помнить, что замена методов во время выполения скрипта требует некоторого времени. Не стоит вызывать Annotator::compile для классов, которые не будут использоваться в данном запросе. Проще всего это реализуется чрез автоматическую загрузку классов php [10].
Для долгоживущих приложений (простые демоны или приложения на основе phpDaemon [11]) вносимый overhead практически не будет влиять на производительность, т.к. классы будут загружены и обработаны всего 1 раз.
Автор: BVadim
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/12697
Ссылки в тексте:
[1] АОП: http://http://ru.wikipedia.org/wiki/%D0%90%D1%81%D0%BF%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5
[2] волшебных методов: http://www.php.net/manual/ru/language.oop5.overloading.php#object.call
[3] runkit: http://ru2.php.net/manual/ru/book.runkit.php
[4] Annotator: https://github.com/baryshev/Annotator
[5] официальном репозитории: https://github.com/zenovich/runkit
[6] другой разработчик: https://github.com/nkresge
[7] https://github.com/nkresge/runkit: https://github.com/nkresge/runkit
[8] Annotator.php: https://github.com/baryshev/Annotator/blob/master/Annotator.php
[9] Dependency injection: http://ru.wikipedia.org/wiki/%D0%92%D0%BD%D0%B5%D0%B4%D1%80%D0%B5%D0%BD%D0%B8%D0%B5_%D0%B7%D0%B0%D0%B2%D0%B8%D1%81%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D0%B8
[10] автоматическую загрузку классов php: http://php.net/manual/ru/language.oop5.autoload.php
[11] phpDaemon: http://phpdaemon.net
Нажмите здесь для печати.